From 5a23545ed8bc284978e1d4ee4ed3dff67f95c124 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sat, 23 May 2026 21:11:59 +0800 Subject: [PATCH 1/4] hwtier system enabled --- base/CMakeLists.txt | 1 + .../system/hardware_tier/hardware_tier.h | 156 +++++++++++++ .../system/hardware_tier/hardware_tier_data.h | 211 +++++++++++++++++ base/system/CMakeLists.txt | 3 +- base/system/hardware_tier/CMakeLists.txt | 34 +++ .../default/default_assessor.cpp | 42 ++++ .../default/default_collector.cpp | 77 +++++++ .../default/default_cpu_scorer.cpp | 62 +++++ .../default/default_display_scorer.cpp | 71 ++++++ .../default/default_gpu_scorer.cpp | 38 +++ .../default/default_memory_scorer.cpp | 40 ++++ .../hardware_tier/default/default_policy.cpp | 62 +++++ base/system/hardware_tier/default_factories.h | 34 +++ base/system/hardware_tier/hardware_tier.cpp | 216 ++++++++++++++++++ .../hardware_tier/hardware_tier_assessor.h | 41 ++++ .../hardware_tier/hardware_tier_collector.h | 40 ++++ .../hardware_tier/hardware_tier_policy.h | 41 ++++ .../hardware_tier/hardware_tier_scorer.h | 40 ++++ example/base/system/CMakeLists.txt | 6 + example/base/system/example_hardware_tier.cpp | 57 +++++ test/system/CMakeLists.txt | 8 + test/system/test_hardware_tier.cpp | 105 +++++++++ 22 files changed, 1384 insertions(+), 1 deletion(-) create mode 100644 base/include/system/hardware_tier/hardware_tier.h create mode 100644 base/include/system/hardware_tier/hardware_tier_data.h create mode 100644 base/system/hardware_tier/CMakeLists.txt create mode 100644 base/system/hardware_tier/default/default_assessor.cpp create mode 100644 base/system/hardware_tier/default/default_collector.cpp create mode 100644 base/system/hardware_tier/default/default_cpu_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_display_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_gpu_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_memory_scorer.cpp create mode 100644 base/system/hardware_tier/default/default_policy.cpp create mode 100644 base/system/hardware_tier/default_factories.h create mode 100644 base/system/hardware_tier/hardware_tier.cpp create mode 100644 base/system/hardware_tier/hardware_tier_assessor.h create mode 100644 base/system/hardware_tier/hardware_tier_collector.h create mode 100644 base/system/hardware_tier/hardware_tier_policy.h create mode 100644 base/system/hardware_tier/hardware_tier_scorer.h create mode 100644 example/base/system/example_hardware_tier.cpp create mode 100644 test/system/test_hardware_tier.cpp diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 0a0f55588..58a8485b4 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -27,6 +27,7 @@ set(CFBASE_MODULE_TARGETS cfbase_network cfbase_gpu cfbase_console + cfbase_hardware_tier ) # Link static libraries from sub-modules with --whole-archive to ensure diff --git a/base/include/system/hardware_tier/hardware_tier.h b/base/include/system/hardware_tier/hardware_tier.h new file mode 100644 index 000000000..1d052bb6e --- /dev/null +++ b/base/include/system/hardware_tier/hardware_tier.h @@ -0,0 +1,156 @@ +/** + * @file hardware_tier.h + * @brief Declares the hardware tier assessment public API. + * + * Provides functions for registering pluggable pipeline stages, + * configuring DeviceConfig overrides, and performing hardware + * tier assessments. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "base/expected/expected.hpp" +#include "base/export.h" +#include "system/hardware_tier/hardware_tier_data.h" + +#include +#include + +namespace cf { + +class IHardwareCollector; +class IHardwareScorer; +class IHardwareAssessor; +class IHardwarePolicy; + +// ───────────────────────────────────────────────────────── +// Pipeline Registry +// ───────────────────────────────────────────────────────── + +/** + * @brief Registers a custom hardware data collector. + * + * Replaces the default collector. Must be called before the first + * assessHardware() invocation. + * + * @param[in] collector Unique pointer to the collector implementation. + * + * @return void (replaces the registered collector). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerCollector(std::unique_ptr collector); + +/** + * @brief Registers a custom scorer for a specific dimension. + * + * @param[in] dimension One of "cpu", "gpu", "memory", "display". + * @param[in] scorer Unique pointer to the scorer implementation. + * + * @return void (replaces the registered scorer for the dimension). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerScorer(std::string_view dimension, + std::unique_ptr scorer); + +/** + * @brief Registers a custom assessor implementation. + * + * @param[in] assessor Unique pointer to the assessor implementation. + * + * @return void (replaces the registered assessor). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerAssessor(std::unique_ptr assessor); + +/** + * @brief Registers a custom policy implementation. + * + * @param[in] policy Unique pointer to the policy implementation. + * + * @return void (replaces the registered policy). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void registerPolicy(std::unique_ptr policy); + +// ───────────────────────────────────────────────────────── +// DeviceConfig Override +// ───────────────────────────────────────────────────────── + +/** + * @brief Sets a manual tier override from DeviceConfig. + * + * When set, the pipeline short-circuits: collection and scoring are + * skipped, and the specified tier level is used directly. + * + * @param[in] level The tier level to enforce. + * @param[in] reason Optional human-readable reason string. + * + * @return void (activates the tier override). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void setDeviceConfigOverride(HardwareTierLevel level, std::string reason = {}); + +/** + * @brief Clears any active DeviceConfig override. + * + * @return void (restores normal pipeline behavior). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT void clearDeviceConfigOverride(); + +// ───────────────────────────────────────────────────────── +// Assessment API +// ───────────────────────────────────────────────────────── + +/** + * @brief Performs a complete hardware tier assessment. + * + * Executes the full pipeline: Collect -> Score -> Assess -> Policy. + * If a DeviceConfig override is active, the pipeline short-circuits + * and returns the overridden tier. + * + * The result is cached after the first successful call. Pass + * force_refresh = true to re-run the pipeline. + * + * @param[in] force_refresh If true, forces re-running the pipeline. + * + * @return expected containing HardwareTierAssessment on success, + * or HardwareTierError on failure. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT expected +assessHardware(bool force_refresh = false); + +/** + * @brief Queries capability flags from the last assessment. + * + * Must be called after a successful assessHardware(). + * + * @return expected containing HardwareTierCapabilities on success, + * or HardwareTierError if no assessment exists. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT expected getHardwareTierCapabilities(); + +} // namespace cf diff --git a/base/include/system/hardware_tier/hardware_tier_data.h b/base/include/system/hardware_tier/hardware_tier_data.h new file mode 100644 index 000000000..706343016 --- /dev/null +++ b/base/include/system/hardware_tier/hardware_tier_data.h @@ -0,0 +1,211 @@ +/** + * @file hardware_tier_data.h + * @brief Defines data structures for hardware tier assessment. + * + * Provides the core types used by the hardware tier pipeline: tier levels, + * per-dimension scores, collected hardware data, assessment results, + * capability flags, and error codes. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "base/export.h" +#include +#include + +namespace cf { + +// ───────────────────────────────────────────────────────── +// Hardware Tier Level +// ───────────────────────────────────────────────────────── + +/** + * @brief Hardware capability tier classification. + * + * Represents the overall capability tier of the device, derived from + * multi-dimensional scoring. + * + * Tier examples: + * - Low: i.MX6ULL (528MHz A7) -- no animation, linuxfb, soft decode + * - Mid: RK3568 (4xA55, Mali-G52) -- partial animation, optional eglfs + * - High: RK3588 (8xA76/A55, Mali-G610) -- full animation, eglfs+OpenGL + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +enum class HardwareTierLevel : uint8_t { + Unknown = 0, ///< Assessment could not be completed. + Low = 1, ///< Minimal capability embedded device. + Mid = 2, ///< Mid-range capability SoC. + High = 3 ///< High capability desktop/workstation grade. +}; + +/** + * @brief Converts HardwareTierLevel to a human-readable string. + * + * @param[in] level The tier level. + * @return "Unknown", "Low", "Mid", or "High". + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +CF_BASE_EXPORT const char* hardwareTierLevelToString(HardwareTierLevel level) noexcept; + +// ───────────────────────────────────────────────────────── +// Per-Dimension Score Types +// ───────────────────────────────────────────────────────── + +/** + * @brief CPU capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct CpuScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief GPU capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct GpuScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief Memory capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct MemoryScore { + int value = 0; ///< Raw score (0-100). +}; + +/** + * @brief Display capability score (0-100). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct DisplayScore { + int value = 0; ///< Raw score (0-100). +}; + +// ───────────────────────────────────────────────────────── +// Collected Hardware Data (raw snapshot) +// ───────────────────────────────────────────────────────── + +/** + * @brief Raw collected hardware data used as scorer input. + * + * Aggregate of all probe results gathered by the collector stage. + * Owns all data independently so it outlives any probe cache. + * Fields that could not be collected are left at defaults. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareData { + // CPU + std::string cpu_model; ///< CPU model name. + std::string cpu_arch; ///< CPU architecture string. + uint16_t logical_cores = 0; ///< Logical CPU thread count. + uint16_t physical_cores = 0; ///< Physical CPU core count. + uint32_t max_frequency = 0; ///< Max frequency in MHz. + bool has_big_little = false; ///< big.LITTLE architecture flag. + uint32_t big_core_count = 0; ///< Number of performance cores. + + // GPU + std::string gpu_name; ///< GPU device name. + uint32_t gpu_vendor_id = 0; ///< PCI vendor ID. + bool gpu_is_discrete = false; ///< Discrete GPU flag. + bool gpu_is_software = false; ///< Software rendering flag. + int existing_gpu_score = 0; ///< Existing EnvironmentScore.gpu value (0-50). + + // Display + int display_width = 0; ///< Display width in pixels. + int display_height = 0; ///< Display height in pixels. + double display_refresh = 0.0; ///< Refresh rate in Hz. + double display_dpi = 96.0; ///< DPI. + int existing_display_score = 0; ///< Existing EnvironmentScore.display value (0-50). + + // Memory + uint64_t total_physical_bytes = 0; ///< Total physical RAM. + uint64_t total_swap_bytes = 0; ///< Total swap space. +}; + +// ───────────────────────────────────────────────────────── +// Assessment Result +// ───────────────────────────────────────────────────────── + +/** + * @brief Complete hardware tier assessment result. + * + * Contains per-dimension scores, the overall tier level, and + * override metadata. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareTierAssessment { + CpuScore cpu; ///< CPU capability score. + GpuScore gpu; ///< GPU capability score. + MemoryScore memory; ///< Memory capability score. + DisplayScore display; ///< Display capability score. + HardwareTierLevel tier = HardwareTierLevel::Unknown; ///< Overall classification. + + bool is_overridden = false; ///< True if DeviceConfig override is active. + std::string override_reason; ///< Override reason (when is_overridden). +}; + +// ───────────────────────────────────────────────────────── +// Capability Flags +// ───────────────────────────────────────────────────────── + +/** + * @brief Capability flags derived from the policy stage. + * + * Drives downstream decisions: rendering backend, animation policy, + * decoder selection, etc. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +struct HardwareTierCapabilities { + bool use_opengl = false; ///< Use OpenGL/EGLFS rendering. + bool use_software_render = false; ///< Use software rendering / linuxfb. + bool enable_animation = false; ///< Enable full UI animations. + bool enable_partial_animation = false; ///< Enable limited animations. + bool use_hardware_decode = false; ///< Use hardware video decoding. + bool use_eglfs = false; ///< Use EGLFS platform plugin. + bool use_linuxfb = false; ///< Use linuxfb platform plugin. +}; + +// ───────────────────────────────────────────────────────── +// Error Codes +// ───────────────────────────────────────────────────────── + +/** + * @brief Error types for hardware tier assessment. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +enum class HardwareTierError { + NoError, ///< Assessment completed successfully. + CollectionFailed, ///< Hardware data collection failed. + ScoringFailed, ///< Scoring stage produced no results. + AssessmentFailed, ///< Assessment stage failed. + PolicyFailed, ///< Policy derivation failed. +}; + +} // namespace cf diff --git a/base/system/CMakeLists.txt b/base/system/CMakeLists.txt index 590492259..effddf00e 100644 --- a/base/system/CMakeLists.txt +++ b/base/system/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(cpu) add_subdirectory(gpu) add_subdirectory(memory) -add_subdirectory(network) \ No newline at end of file +add_subdirectory(network) +add_subdirectory(hardware_tier) \ No newline at end of file diff --git a/base/system/hardware_tier/CMakeLists.txt b/base/system/hardware_tier/CMakeLists.txt new file mode 100644 index 000000000..602ee0f9e --- /dev/null +++ b/base/system/hardware_tier/CMakeLists.txt @@ -0,0 +1,34 @@ +# Hardware Tier Assessment Module +cmake_minimum_required(VERSION 3.16) + +add_library(cfbase_hardware_tier STATIC) +set_target_properties(cfbase_hardware_tier PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +target_compile_definitions(cfbase_hardware_tier PRIVATE CFBASE_EXPORTS) + +target_include_directories(cfbase_hardware_tier + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. +) + +target_sources(cfbase_hardware_tier PRIVATE + hardware_tier.cpp + default/default_collector.cpp + default/default_cpu_scorer.cpp + default/default_gpu_scorer.cpp + default/default_memory_scorer.cpp + default/default_display_scorer.cpp + default/default_assessor.cpp + default/default_policy.cpp +) + +target_link_libraries(cfbase_hardware_tier PRIVATE + cfbase_cpu + cfbase_gpu + cfbase_memory +) diff --git a/base/system/hardware_tier/default/default_assessor.cpp b/base/system/hardware_tier/default/default_assessor.cpp new file mode 100644 index 000000000..9ff25a204 --- /dev/null +++ b/base/system/hardware_tier/default/default_assessor.cpp @@ -0,0 +1,42 @@ +/** + * @file default_assessor.cpp + * @brief Default assessor implementation. + * + * Uses a bottleneck-weighted approach: the lowest dimension caps the + * tier while the average provides a baseline. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultAssessor : public IHardwareAssessor { + public: + HardwareTierLevel assess(int cpu, int gpu, int memory, int display) override { + int minScore = std::min({cpu, gpu, memory, display}); + int avg = (cpu + gpu + memory + display) / 4; + + // 60% bottleneck weight, 40% average + int effective = static_cast(minScore * 0.6 + avg * 0.4); + + if (effective >= 65) + return HardwareTierLevel::High; + if (effective >= 30) + return HardwareTierLevel::Mid; + return HardwareTierLevel::Low; + } +}; + +std::unique_ptr makeDefaultAssessor() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_collector.cpp b/base/system/hardware_tier/default/default_collector.cpp new file mode 100644 index 000000000..4088dfb46 --- /dev/null +++ b/base/system/hardware_tier/default/default_collector.cpp @@ -0,0 +1,77 @@ +/** + * @file default_collector.cpp + * @brief Default hardware data collector implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include "system/cpu/cfcpu.h" +#include "system/cpu/cfcpu_bonus.h" +#include "system/cpu/cfcpu_profile.h" +#include "system/gpu/gpu.h" +#include "system/memory/memory_info.h" + +namespace cf { + +class DefaultCollector : public IHardwareCollector { + public: + HardwareData collect() override { + HardwareData data; + collectCpu(data); + collectGpu(data); + collectMemory(data); + return data; + } + + private: + void collectCpu(HardwareData& data) { + if (auto info = getCPUInfo(); info.has_value()) { + data.cpu_model = std::string(info->model); + data.cpu_arch = std::string(info->arch); + } + if (auto profile = getCPUProfileInfo(); profile.has_value()) { + data.logical_cores = profile->logical_cnt; + data.physical_cores = profile->physical_cnt; + data.max_frequency = profile->max_frequency; + } + if (auto bonus = getCPUBonusInfo(); bonus.has_value()) { + data.has_big_little = bonus->has_big_little; + data.big_core_count = bonus->big_core_count; + } + } + + void collectGpu(HardwareData& data) { + if (auto gd = getGpuDisplayInfo(); gd.has_value()) { + data.gpu_name = gd->gpu.name; + data.gpu_vendor_id = gd->gpu.vendorId; + data.gpu_is_discrete = gd->gpu.isDiscrete; + data.gpu_is_software = gd->gpu.hasSoftware; + data.existing_gpu_score = gd->score.gpu; + + data.display_width = gd->display.width; + data.display_height = gd->display.height; + data.display_refresh = gd->display.refreshRate; + data.display_dpi = gd->display.dpi; + data.existing_display_score = gd->score.display; + } + } + + void collectMemory(HardwareData& data) { + MemoryInfo mem; + getSystemMemoryInfo(mem); + data.total_physical_bytes = mem.physical.total_bytes; + data.total_swap_bytes = mem.swap.total_bytes; + } +}; + +std::unique_ptr makeDefaultCollector() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_cpu_scorer.cpp b/base/system/hardware_tier/default/default_cpu_scorer.cpp new file mode 100644 index 000000000..8e5883e70 --- /dev/null +++ b/base/system/hardware_tier/default/default_cpu_scorer.cpp @@ -0,0 +1,62 @@ +/** + * @file default_cpu_scorer.cpp + * @brief Default CPU dimension scorer implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultCpuScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + int s = 0; + + // Core count (max 40 points) + if (data.logical_cores >= 8) + s += 40; + else if (data.logical_cores >= 4) + s += 25; + else if (data.logical_cores >= 2) + s += 15; + else + s += 5; + + // Frequency (max 35 points) + if (data.max_frequency >= 2400) + s += 35; + else if (data.max_frequency >= 1800) + s += 25; + else if (data.max_frequency >= 1000) + s += 15; + else + s += 5; + + // Architecture bonus (max 25 points) + if (data.has_big_little && data.big_core_count >= 4) + s += 25; + else if (data.has_big_little) + s += 15; + else if (data.cpu_arch.find("aarch64") != std::string::npos || + data.cpu_arch.find("x86_64") != std::string::npos) + s += 20; + else + s += 5; + + return std::clamp(s, 0, 100); + } +}; + +std::unique_ptr makeDefaultCpuScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_display_scorer.cpp b/base/system/hardware_tier/default/default_display_scorer.cpp new file mode 100644 index 000000000..a41648117 --- /dev/null +++ b/base/system/hardware_tier/default/default_display_scorer.cpp @@ -0,0 +1,71 @@ +/** + * @file default_display_scorer.cpp + * @brief Default display dimension scorer implementation. + * + * Adapts the existing EnvironmentScore.display (0-50) to 0-100 range. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultDisplayScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + if (data.existing_display_score > 0) { + return std::min(data.existing_display_score * 2, 100); + } + + // Fallback: compute from raw display data + auto pixels = static_cast(data.display_width) * data.display_height; + int s = 0; + + // Resolution (max 50 points) + if (pixels >= 3840LL * 2160) + s += 50; + else if (pixels >= 2560LL * 1440) + s += 40; + else if (pixels >= 1920LL * 1080) + s += 30; + else if (pixels >= 1280LL * 720) + s += 15; + else + s += 5; + + // Refresh rate (max 30 points) + if (data.display_refresh >= 120) + s += 30; + else if (data.display_refresh >= 90) + s += 25; + else if (data.display_refresh >= 60) + s += 20; + else + s += 5; + + // DPI (max 20 points) + if (data.display_dpi >= 200) + s += 20; + else if (data.display_dpi >= 150) + s += 15; + else if (data.display_dpi >= 96) + s += 10; + else + s += 3; + + return std::min(s, 100); + } +}; + +std::unique_ptr makeDefaultDisplayScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_gpu_scorer.cpp b/base/system/hardware_tier/default/default_gpu_scorer.cpp new file mode 100644 index 000000000..410f6892e --- /dev/null +++ b/base/system/hardware_tier/default/default_gpu_scorer.cpp @@ -0,0 +1,38 @@ +/** + * @file default_gpu_scorer.cpp + * @brief Default GPU dimension scorer implementation. + * + * Adapts the existing EnvironmentScore.gpu (0-50) to 0-100 range. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultGpuScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + if (data.gpu_is_software) + return 5; + if (data.gpu_vendor_id == 0) + return 5; + + // Normalize existing 0-50 score to 0-100 + int base = data.existing_gpu_score * 2; + return std::min(base, 100); + } +}; + +std::unique_ptr makeDefaultGpuScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_memory_scorer.cpp b/base/system/hardware_tier/default/default_memory_scorer.cpp new file mode 100644 index 000000000..85e2bf1a1 --- /dev/null +++ b/base/system/hardware_tier/default/default_memory_scorer.cpp @@ -0,0 +1,40 @@ +/** + * @file default_memory_scorer.cpp + * @brief Default memory dimension scorer implementation. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +#include + +namespace cf { + +class DefaultMemoryScorer : public IHardwareScorer { + public: + int score(const HardwareData& data) override { + auto gb = static_cast(data.total_physical_bytes / (1024ULL * 1024 * 1024)); + if (gb >= 16) + return 100; + if (gb >= 8) + return 75; + if (gb >= 4) + return 55; + if (gb >= 2) + return 35; + if (gb >= 1) + return 20; + return 5; + } +}; + +std::unique_ptr makeDefaultMemoryScorer() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default/default_policy.cpp b/base/system/hardware_tier/default/default_policy.cpp new file mode 100644 index 000000000..2439be2a5 --- /dev/null +++ b/base/system/hardware_tier/default/default_policy.cpp @@ -0,0 +1,62 @@ +/** + * @file default_policy.cpp + * @brief Default policy implementation. + * + * Maps HardwareTierLevel to HardwareTierCapabilities based on the + * design document tier definitions. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "../default_factories.h" + +namespace cf { + +class DefaultPolicy : public IHardwarePolicy { + public: + HardwareTierCapabilities deriveCapabilities(HardwareTierLevel level, + const HardwareTierAssessment& assessment) override { + HardwareTierCapabilities caps; + + switch (level) { + case HardwareTierLevel::High: + caps.use_opengl = true; + caps.enable_animation = true; + caps.use_hardware_decode = true; + caps.use_eglfs = true; + break; + + case HardwareTierLevel::Mid: + caps.use_opengl = true; + caps.enable_partial_animation = true; + caps.use_hardware_decode = true; + caps.use_eglfs = (assessment.gpu.value >= 40); + caps.use_linuxfb = !caps.use_eglfs; + break; + + case HardwareTierLevel::Low: + caps.use_software_render = true; + caps.use_hardware_decode = false; + caps.use_linuxfb = true; + break; + + case HardwareTierLevel::Unknown: + default: + caps.use_software_render = true; + caps.use_linuxfb = true; + break; + } + + return caps; + } +}; + +std::unique_ptr makeDefaultPolicy() { + return std::make_unique(); +} + +} // namespace cf diff --git a/base/system/hardware_tier/default_factories.h b/base/system/hardware_tier/default_factories.h new file mode 100644 index 000000000..83faab929 --- /dev/null +++ b/base/system/hardware_tier/default_factories.h @@ -0,0 +1,34 @@ +/** + * @file default_factories.h + * @brief Declares factory functions for default pipeline implementations. + * + * Each default implementation .cpp file provides a factory function + * that creates the appropriate unique_ptr, avoiding the need for + * full class definitions in the pipeline executor. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "hardware_tier_assessor.h" +#include "hardware_tier_collector.h" +#include "hardware_tier_policy.h" +#include "hardware_tier_scorer.h" + +#include + +namespace cf { + +std::unique_ptr makeDefaultCollector(); +std::unique_ptr makeDefaultCpuScorer(); +std::unique_ptr makeDefaultGpuScorer(); +std::unique_ptr makeDefaultMemoryScorer(); +std::unique_ptr makeDefaultDisplayScorer(); +std::unique_ptr makeDefaultAssessor(); +std::unique_ptr makeDefaultPolicy(); + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier.cpp b/base/system/hardware_tier/hardware_tier.cpp new file mode 100644 index 000000000..beee9dc0a --- /dev/null +++ b/base/system/hardware_tier/hardware_tier.cpp @@ -0,0 +1,216 @@ +/** + * @file hardware_tier.cpp + * @brief Implements the hardware tier assessment pipeline. + * + * Contains the registry, caching logic, and pipeline orchestration + * for the Collect -> Score -> Assess -> Policy workflow. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include "hardware_tier_assessor.h" +#include "hardware_tier_collector.h" +#include "hardware_tier_policy.h" +#include "hardware_tier_scorer.h" + +#include "default_factories.h" + +#include +#include +#include +#include +#include + +namespace cf { + +// ───────────────────────────────────────────────────────── +// Pipeline Registry +// ───────────────────────────────────────────────────────── +struct HardwareTierRegistry { + std::unique_ptr collector; + std::unique_ptr cpu_scorer; + std::unique_ptr gpu_scorer; + std::unique_ptr memory_scorer; + std::unique_ptr display_scorer; + std::unique_ptr assessor; + std::unique_ptr policy; + + std::optional override_level; + std::string override_reason; + + HardwareTierRegistry(); +}; + +static HardwareTierRegistry& getRegistry() { + static HardwareTierRegistry reg; + return reg; +} + +// ───────────────────────────────────────────────────────── +// Cached Assessment State +// ───────────────────────────────────────────────────────── +struct CachedAssessment { + HardwareTierAssessment assessment; + HardwareTierCapabilities capabilities; + bool valid = false; +}; + +static CachedAssessment g_cached; +static std::mutex g_cache_mutex; + +// ───────────────────────────────────────────────────────── +// Public API +// ───────────────────────────────────────────────────────── + +const char* hardwareTierLevelToString(HardwareTierLevel level) noexcept { + switch (level) { + case HardwareTierLevel::Low: + return "Low"; + case HardwareTierLevel::Mid: + return "Mid"; + case HardwareTierLevel::High: + return "High"; + default: + return "Unknown"; + } +} + +void invalidateCache() { + std::lock_guard lock(g_cache_mutex); + g_cached.valid = false; +} + +void registerCollector(std::unique_ptr collector) { + getRegistry().collector = std::move(collector); + invalidateCache(); +} + +void registerScorer(std::string_view dimension, std::unique_ptr scorer) { + auto& reg = getRegistry(); + if (dimension == "cpu") + reg.cpu_scorer = std::move(scorer); + else if (dimension == "gpu") + reg.gpu_scorer = std::move(scorer); + else if (dimension == "memory") + reg.memory_scorer = std::move(scorer); + else if (dimension == "display") + reg.display_scorer = std::move(scorer); + invalidateCache(); +} + +void registerAssessor(std::unique_ptr assessor) { + getRegistry().assessor = std::move(assessor); + invalidateCache(); +} + +void registerPolicy(std::unique_ptr policy) { + getRegistry().policy = std::move(policy); + invalidateCache(); +} + +void setDeviceConfigOverride(HardwareTierLevel level, std::string reason) { + auto& reg = getRegistry(); + reg.override_level = level; + reg.override_reason = std::move(reason); + invalidateCache(); +} + +void clearDeviceConfigOverride() { + auto& reg = getRegistry(); + reg.override_level.reset(); + reg.override_reason.clear(); + invalidateCache(); +} + +expected assessHardware(bool force_refresh) { + if (force_refresh) { + invalidateCache(); + } + + { + std::lock_guard lock(g_cache_mutex); + if (g_cached.valid) { + return g_cached.assessment; + } + } + + auto& reg = getRegistry(); + HardwareTierAssessment result; + + // ── DeviceConfig override short-circuit ── + if (reg.override_level.has_value()) { + result.tier = *reg.override_level; + result.is_overridden = true; + result.override_reason = reg.override_reason; + + HardwareTierCapabilities caps; + if (reg.policy) { + caps = reg.policy->deriveCapabilities(result.tier, result); + } + std::lock_guard lock(g_cache_mutex); + g_cached.assessment = result; + g_cached.capabilities = caps; + g_cached.valid = true; + return result; + } + + // ── Stage 1: Collect ── + if (!reg.collector) { + return cf::unexpected(HardwareTierError::CollectionFailed); + } + HardwareData data = reg.collector->collect(); + + // ── Stage 2: Score ── + if (!reg.cpu_scorer || !reg.gpu_scorer || !reg.memory_scorer || !reg.display_scorer) { + return cf::unexpected(HardwareTierError::ScoringFailed); + } + + result.cpu.value = reg.cpu_scorer->score(data); + result.gpu.value = reg.gpu_scorer->score(data); + result.memory.value = reg.memory_scorer->score(data); + result.display.value = reg.display_scorer->score(data); + + // ── Stage 3: Assess ── + if (!reg.assessor) { + return cf::unexpected(HardwareTierError::AssessmentFailed); + } + result.tier = reg.assessor->assess(result.cpu.value, result.gpu.value, result.memory.value, + result.display.value); + + // ── Stage 4: Policy ── + HardwareTierCapabilities caps; + if (reg.policy) { + caps = reg.policy->deriveCapabilities(result.tier, result); + } + + std::lock_guard lock(g_cache_mutex); + g_cached.assessment = result; + g_cached.capabilities = caps; + g_cached.valid = true; + return result; +} + +expected getHardwareTierCapabilities() { + std::lock_guard lock(g_cache_mutex); + if (!g_cached.valid) { + return cf::unexpected(HardwareTierError::PolicyFailed); + } + return g_cached.capabilities; +} + +// ───────────────────────────────────────────────────────── +// Registry Constructor — installs all defaults +// ───────────────────────────────────────────────────────── +HardwareTierRegistry::HardwareTierRegistry() + : collector(makeDefaultCollector()), cpu_scorer(makeDefaultCpuScorer()), + gpu_scorer(makeDefaultGpuScorer()), memory_scorer(makeDefaultMemoryScorer()), + display_scorer(makeDefaultDisplayScorer()), assessor(makeDefaultAssessor()), + policy(makeDefaultPolicy()) {} + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_assessor.h b/base/system/hardware_tier/hardware_tier_assessor.h new file mode 100644 index 000000000..7b2b63ef3 --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_assessor.h @@ -0,0 +1,41 @@ +/** + * @file hardware_tier_assessor.h + * @brief Declares the IHardwareAssessor interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the assessment stage. + * + * Maps multi-dimensional scores to an overall HardwareTierLevel. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareAssessor { + public: + virtual ~IHardwareAssessor() = default; + + /** + * @brief Determines the overall tier level from per-dimension scores. + * + * @param[in] cpu CPU dimension score. + * @param[in] gpu GPU dimension score. + * @param[in] memory Memory dimension score. + * @param[in] display Display dimension score. + * @return HardwareTierLevel classification. + */ + virtual HardwareTierLevel assess(int cpu, int gpu, int memory, int display) = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_collector.h b/base/system/hardware_tier/hardware_tier_collector.h new file mode 100644 index 000000000..546813858 --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_collector.h @@ -0,0 +1,40 @@ +/** + * @file hardware_tier_collector.h + * @brief Declares the IHardwareCollector interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the hardware data collection stage. + * + * Implementations gather raw hardware data from platform probes. + * The default implementation calls the existing cf::getCPUInfo(), + * cf::getGpuDisplayInfo(), cf::getSystemMemoryInfo() probes. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareCollector { + public: + virtual ~IHardwareCollector() = default; + + /** + * @brief Collects raw hardware data from all subsystems. + * + * @return HardwareData populated with probe results. + * Fields that could not be collected are left at defaults. + */ + virtual HardwareData collect() = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_policy.h b/base/system/hardware_tier/hardware_tier_policy.h new file mode 100644 index 000000000..9abcb146a --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_policy.h @@ -0,0 +1,41 @@ +/** + * @file hardware_tier_policy.h + * @brief Declares the IHardwarePolicy interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the policy stage. + * + * Maps an HardwareTierLevel to concrete capability flags that drive + * downstream behavior (rendering backend, animations, etc.). + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwarePolicy { + public: + virtual ~IHardwarePolicy() = default; + + /** + * @brief Derives capability flags from the tier level. + * + * @param[in] level The assessed tier level. + * @param[in] assessment The full assessment (for dimension-aware policy). + * @return HardwareTierCapabilities flags. + */ + virtual HardwareTierCapabilities + deriveCapabilities(HardwareTierLevel level, const HardwareTierAssessment& assessment) = 0; +}; + +} // namespace cf diff --git a/base/system/hardware_tier/hardware_tier_scorer.h b/base/system/hardware_tier/hardware_tier_scorer.h new file mode 100644 index 000000000..1a74eec4c --- /dev/null +++ b/base/system/hardware_tier/hardware_tier_scorer.h @@ -0,0 +1,40 @@ +/** + * @file hardware_tier_scorer.h + * @brief Declares the IHardwareScorer interface. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ +#pragma once + +#include "system/hardware_tier/hardware_tier_data.h" + +namespace cf { + +/** + * @brief Interface for the per-dimension scoring stage. + * + * Each dimension (cpu, gpu, memory, display) gets its own scorer + * registered via registerScorer(). Scorers map raw collected data + * to a 0-100 score. + * + * @since 0.19 + * @ingroup system_hardware_tier + */ +class IHardwareScorer { + public: + virtual ~IHardwareScorer() = default; + + /** + * @brief Scores a specific dimension from the collected data. + * + * @param[in] data The collected hardware data. + * @return Integer score in the range [0, 100]. + */ + virtual int score(const HardwareData& data) = 0; +}; + +} // namespace cf diff --git a/example/base/system/CMakeLists.txt b/example/base/system/CMakeLists.txt index 281b29f24..8a55f867f 100644 --- a/example/base/system/CMakeLists.txt +++ b/example/base/system/CMakeLists.txt @@ -25,3 +25,9 @@ add_executable(example_gpu_info example_gpu_info.cpp) target_link_libraries(example_gpu_info PRIVATE cfbase) cf_set_example_output_dir(example_gpu_info "base") cf_register_example_launcher(example_gpu_info "base") + +# Hardware Tier Assessment +add_executable(example_hardware_tier example_hardware_tier.cpp) +target_link_libraries(example_hardware_tier PRIVATE cfbase) +cf_set_example_output_dir(example_hardware_tier "base") +cf_register_example_launcher(example_hardware_tier "base") diff --git a/example/base/system/example_hardware_tier.cpp b/example/base/system/example_hardware_tier.cpp new file mode 100644 index 000000000..2533c46a0 --- /dev/null +++ b/example/base/system/example_hardware_tier.cpp @@ -0,0 +1,57 @@ +/** + * @file example_hardware_tier.cpp + * @brief Demonstrates hardware tier assessment on the current machine. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include + +using namespace cf; + +int main() { + auto assessment = assessHardware(); + if (!assessment.has_value()) { + std::cerr << "Failed to assess hardware tier, error: " + << static_cast(assessment.error()) << "\n"; + return 1; + } + + const auto& a = *assessment; + + std::cout << "========== Hardware Tier Assessment ==========\n"; + std::cout << "CPU Score: " << a.cpu.value << " / 100\n"; + std::cout << "GPU Score: " << a.gpu.value << " / 100\n"; + std::cout << "Memory Score: " << a.memory.value << " / 100\n"; + std::cout << "Display Score: " << a.display.value << " / 100\n"; + std::cout << "\n"; + std::cout << "Overall Tier: " << hardwareTierLevelToString(a.tier) << "\n"; + + if (a.is_overridden) { + std::cout << " (Overridden: " << a.override_reason << ")\n"; + } + + auto caps = getHardwareTierCapabilities(); + if (caps.has_value()) { + const auto& c = *caps; + std::cout << "\n========== Capability Flags ==========\n"; + std::cout << "OpenGL: " << (c.use_opengl ? "Yes" : "No") << "\n"; + std::cout << "Software Render: " << (c.use_software_render ? "Yes" : "No") << "\n"; + std::cout << "Animation: " + << (c.enable_animation ? "Full" + : c.enable_partial_animation ? "Partial" + : "None") + << "\n"; + std::cout << "Hardware Decode: " << (c.use_hardware_decode ? "Yes" : "No") << "\n"; + std::cout << "EGLFS: " << (c.use_eglfs ? "Yes" : "No") << "\n"; + std::cout << "LinuxFB: " << (c.use_linuxfb ? "Yes" : "No") << "\n"; + } + + return 0; +} diff --git a/test/system/CMakeLists.txt b/test/system/CMakeLists.txt index d8b4b9e07..06c2f2542 100644 --- a/test/system/CMakeLists.txt +++ b/test/system/CMakeLists.txt @@ -20,3 +20,11 @@ add_gtest_executable( LABELS "base;system" LOG_MODULE base_system ) + +add_gtest_executable( + TEST_NAME test_hardware_tier + SOURCE_FILE test_hardware_tier.cpp + LINK_LIBRARIES cfbase;GTest::gtest;GTest::gtest_main + LABELS "base;system" + LOG_MODULE base_system +) diff --git a/test/system/test_hardware_tier.cpp b/test/system/test_hardware_tier.cpp new file mode 100644 index 000000000..2b610a5c3 --- /dev/null +++ b/test/system/test_hardware_tier.cpp @@ -0,0 +1,105 @@ +/** + * @file test_hardware_tier.cpp + * @brief Unit tests for the hardware tier assessment pipeline. + * + * @author Charliechen114514 + * @date 2026-05-23 + * @version 0.19 + * @since 0.19 + * @ingroup system_hardware_tier + */ + +#include "system/hardware_tier/hardware_tier.h" + +#include + +using namespace cf; + +// ── Basic Assessment ──────────────────────────────────── + +TEST(HardwareTierTest, AssessReturnsValidResult) { + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + + const auto& a = *result; + EXPECT_GE(a.cpu.value, 0); + EXPECT_LE(a.cpu.value, 100); + EXPECT_GE(a.gpu.value, 0); + EXPECT_LE(a.gpu.value, 100); + EXPECT_GE(a.memory.value, 0); + EXPECT_LE(a.memory.value, 100); + EXPECT_GE(a.display.value, 0); + EXPECT_LE(a.display.value, 100); + + EXPECT_NE(a.tier, HardwareTierLevel::Unknown); +} + +// ── Tier Level Strings ────────────────────────────────── + +TEST(HardwareTierTest, TierLevelStrings) { + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Low), "Low"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Mid), "Mid"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::High), "High"); + EXPECT_STREQ(hardwareTierLevelToString(HardwareTierLevel::Unknown), "Unknown"); +} + +// ── Capabilities Accessible ───────────────────────────── + +TEST(HardwareTierTest, CapabilitiesAccessible) { + assessHardware(true); + + auto caps = getHardwareTierCapabilities(); + ASSERT_TRUE(caps.has_value()); + EXPECT_TRUE(caps->use_opengl || caps->use_software_render || caps->use_eglfs || + caps->use_linuxfb); +} + +// ── DeviceConfig Override ─────────────────────────────── + +TEST(HardwareTierTest, DeviceConfigOverride) { + setDeviceConfigOverride(HardwareTierLevel::Mid, "test override"); + + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->tier, HardwareTierLevel::Mid); + EXPECT_TRUE(result->is_overridden); + EXPECT_EQ(result->override_reason, "test override"); + + clearDeviceConfigOverride(); + assessHardware(true); +} + +// ── Caching Behavior ──────────────────────────────────── + +TEST(HardwareTierTest, CachingReturnsSameResult) { + auto first = assessHardware(); + ASSERT_TRUE(first.has_value()); + + auto second = assessHardware(); + ASSERT_TRUE(second.has_value()); + + EXPECT_EQ(first->cpu.value, second->cpu.value); + EXPECT_EQ(first->tier, second->tier); +} + +// ── Force Refresh ─────────────────────────────────────── + +TEST(HardwareTierTest, ForceRefreshProducesResult) { + auto result = assessHardware(true); + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result->tier, HardwareTierLevel::Unknown); +} + +// ── Override Clear Restores Normal Assessment ─────────── + +TEST(HardwareTierTest, OverrideClearRestoresNormal) { + setDeviceConfigOverride(HardwareTierLevel::Low, "forced low"); + auto overridden = assessHardware(true); + ASSERT_TRUE(overridden.has_value()); + EXPECT_EQ(overridden->tier, HardwareTierLevel::Low); + + clearDeviceConfigOverride(); + auto normal = assessHardware(true); + ASSERT_TRUE(normal.has_value()); + EXPECT_FALSE(normal->is_overridden); +} From 7ccb63cdaf3004751d2d95706353af0c1e0635d9 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sat, 23 May 2026 22:00:59 +0800 Subject: [PATCH 2/4] refactor: refactor the ui subsystem --- CMakeLists.txt | 2 +- README.md | 2 +- .../include/cfconfig/cfconfig_path_provider.h | 10 +- .../src/cfconfig_path_provider.cpp | 8 +- .../base/config_manager/01-quick-start.md | 2 +- .../desktop/base/config_manager/03-faq.md | 24 +- .../base/config_manager/04-architecture.md | 4 +- .../desktop/base/config_manager/README.md | 2 +- .../design_stage/01_phase1_hardware_probe.md | 13 +- .../design_stage/02_phase2_base_library.md | 4 +- document/todo/desktop/07_render_backend.md | 4 +- .../todo/done/01_hardware_probe_status.md | 299 ++--- document/todo/done/02_base_library_status.md | 2 +- .../todo/done/06_infrastructure_status.md | 2 +- document/todo/done/PROJECT_STATUS_REPORT.md | 21 +- test/ui/core/CMakeLists.txt | 13 +- test/ui/core/token_test.cpp | 579 --------- ui/core/CMakeLists.txt | 2 +- ui/core/material/cfmaterial_fonttype.cpp | 200 +-- ui/core/material/cfmaterial_fonttype.h | 235 +--- ui/core/material/cfmaterial_motion.cpp | 66 +- ui/core/material/cfmaterial_motion.h | 123 +- ui/core/material/cfmaterial_radius_scale.cpp | 41 +- ui/core/material/cfmaterial_radius_scale.h | 89 +- ui/core/material/cfmaterial_scheme.cpp | 32 +- ui/core/material/cfmaterial_scheme.h | 211 +--- ui/core/material/material_factory.cpp | 241 ++-- ui/core/material/material_factory.hpp | 1 + ui/core/token.hpp | 1018 ---------------- ui/widget/CMakeLists.txt | 1 + .../material/base/material_widget_base.cpp | 95 ++ .../material/base/material_widget_base.h | 214 ++++ ui/widget/material/base/state_machine.cpp | 145 +-- ui/widget/material/base/state_machine.h | 17 + ui/widget/material/widget/button/button.cpp | 140 +-- ui/widget/material/widget/button/button.h | 17 +- .../material/widget/checkbox/checkbox.cpp | 179 +-- ui/widget/material/widget/checkbox/checkbox.h | 691 ++++++----- .../material/widget/comboBox/combobox.cpp | 112 +- ui/widget/material/widget/comboBox/combobox.h | 15 +- .../widget/doublespinbox/doublespinbox.cpp | 89 +- .../widget/doublespinbox/doublespinbox.h | 15 +- .../material/widget/listview/listview.cpp | 100 +- ui/widget/material/widget/listview/listview.h | 15 +- .../widget/progressbar/progressbar.cpp | 69 +- .../material/widget/progressbar/progressbar.h | 13 +- .../widget/radiobutton/radiobutton.cpp | 186 +-- .../material/widget/radiobutton/radiobutton.h | 691 ++++++----- ui/widget/material/widget/slider/slider.cpp | 212 +--- ui/widget/material/widget/slider/slider.h | 17 +- ui/widget/material/widget/spinbox/spinbox.cpp | 90 +- ui/widget/material/widget/spinbox/spinbox.h | 15 +- ui/widget/material/widget/switch/switch.cpp | 188 +-- ui/widget/material/widget/switch/switch.h | 17 +- .../material/widget/tableview/tableview.cpp | 109 +- .../material/widget/tableview/tableview.h | 15 +- .../widget/tabview/private/materialtabbar.cpp | 61 +- .../widget/tabview/private/materialtabbar.h | 12 +- .../material/widget/textarea/textarea.cpp | 86 +- ui/widget/material/widget/textarea/textarea.h | 15 +- .../material/widget/textfield/textfield.cpp | 93 +- .../material/widget/textfield/textfield.h | 1068 ++++++++--------- .../material/widget/treeview/treeview.cpp | 107 +- ui/widget/material/widget/treeview/treeview.h | 15 +- 64 files changed, 2474 insertions(+), 5700 deletions(-) delete mode 100644 test/ui/core/token_test.cpp delete mode 100644 ui/core/token.hpp create mode 100644 ui/widget/material/base/material_widget_base.cpp create mode 100644 ui/widget/material/base/material_widget_base.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0af766211..bae2d248e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ message("Including Requested CMake Dependencies OK") include(cmake/check_toolchain.cmake) project(CFDesktop - VERSION 0.18.0 + VERSION 0.19.0 DESCRIPTION "CFDesktop: Given Your Device Portable Desktop Anywhere" HOMEPAGE_URL "https://github.com/Awesome-Embedded-Learning-Studio/CFDesktop" LANGUAGES CXX C diff --git a/README.md b/README.md index d701455f5..2ba6ae1c9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### 为嵌入式设备打造的现代化 Material Design 3 桌面框架 - [![License: MIT][license-badge]] [![Version: 0.18.0][version-badge]] + [![License: MIT][license-badge]] [![Version: 0.19.0][version-badge]] [![C++23][cpp-badge]] [![Qt 6.8][qt-badge]] [![CMake][cmake-badge]] [![Documentation][docs-badge]] diff --git a/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h b/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h index 6fda4b58c..2e815d54d 100644 --- a/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h +++ b/desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h @@ -16,7 +16,7 @@ namespace cf::config { * @brief Abstract interface for providing config store file paths. * * This interface allows different projects to provide their own path logic: - * - Desktop project: uses standard paths like /etc/cfdesktop, ~/.config/cfdesktop + * - Desktop project: uses paths under the application-managed directory * - Other projects: can use custom paths * - Tests: can use mock/temporary paths * @@ -29,7 +29,7 @@ class IConfigStorePathProvider { /** * @brief Get the system-level config file path. * - * @return Full path to system config (e.g., "/etc/cfdesktop/system.ini") + * @return Full path to system config (e.g., "{app_dir}/system.ini") * @return Empty QString if system layer should be disabled * @throws None * @note None @@ -133,9 +133,9 @@ class IConfigStorePathProvider { * @brief Default path provider for CFDesktop project. * * Provides standard paths: - * - System: /etc/cfdesktop/system.ini + * - System: {app_dir}/system.ini (managed within CFDesktop's own directory) * - User: ~/.config/cfdesktop/user.ini - * - App: config/app.ini (relative to working directory) + * - App: {app_dir}/app.ini */ class DesktopConfigStorePathProvider : public IConfigStorePathProvider { public: @@ -143,7 +143,7 @@ class DesktopConfigStorePathProvider : public IConfigStorePathProvider { * @brief Construct with default paths. * * Uses: - * - system_path: "/etc/cfdesktop/system.ini" + * - system_path: disabled (empty) by default * - user_dir: "~/.config/cfdesktop" * - app_dir: "config" * @throws None diff --git a/desktop/base/config_manager/src/cfconfig_path_provider.cpp b/desktop/base/config_manager/src/cfconfig_path_provider.cpp index 9dc484302..b13576437 100644 --- a/desktop/base/config_manager/src/cfconfig_path_provider.cpp +++ b/desktop/base/config_manager/src/cfconfig_path_provider.cpp @@ -16,17 +16,17 @@ namespace cf::config { // ============================================================================ DesktopConfigStorePathProvider::DesktopConfigStorePathProvider() - : system_path_("/etc/cfdesktop/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), + : system_path_(QString()), user_dir_(QDir::homePath() + "/.config/cfdesktop"), app_dir_("config") {} DesktopConfigStorePathProvider::DesktopConfigStorePathProvider(const QString& app_base) - : system_path_("/etc/cfdesktop/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), + : system_path_(app_base + "/system.ini"), user_dir_(QDir::homePath() + "/.config/cfdesktop"), app_dir_(app_base) {} DesktopConfigStorePathProvider::DesktopConfigStorePathProvider(const QString& organization, const QString& application) - : system_path_("/etc/" + organization + "/system.ini"), - user_dir_(QDir::homePath() + "/.config/" + organization), app_dir_("config") { + : system_path_(QString()), user_dir_(QDir::homePath() + "/.config/" + organization), + app_dir_("config") { user_filename_ = application + ".ini"; app_filename_ = application + ".ini"; } diff --git a/document/HandBook/desktop/base/config_manager/01-quick-start.md b/document/HandBook/desktop/base/config_manager/01-quick-start.md index 01ce124c5..e14e45e90 100644 --- a/document/HandBook/desktop/base/config_manager/01-quick-start.md +++ b/document/HandBook/desktop/base/config_manager/01-quick-start.md @@ -36,7 +36,7 @@ auto& config = ConfigStore::instance(); class CustomPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { - return "/etc/myapp/system.ini"; + return "{app_dir}/system.ini"; // CFDesktop 自管理目录内 } QString user_dir() const override { diff --git a/document/HandBook/desktop/base/config_manager/03-faq.md b/document/HandBook/desktop/base/config_manager/03-faq.md index 523043189..b584b84f8 100644 --- a/document/HandBook/desktop/base/config_manager/03-faq.md +++ b/document/HandBook/desktop/base/config_manager/03-faq.md @@ -108,6 +108,8 @@ ConfigStore::instance().set( #### 方法 2:自定义路径提供者 +> **注**: 以下示例展示如何为自定义应用实现路径提供者。CFDesktop 默认实现使用应用自管理目录,不读取 `/etc/` 系统路径。 + ```cpp #include #include @@ -149,6 +151,8 @@ ConfigStore::instance().initialize(std::make_shared()); #### 方法 3:使用环境变量 +> **注**: 以下为自定义应用的示例。CFDesktop 默认实现使用应用自管理目录。 + ```cpp #include #include @@ -600,7 +604,7 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); | 平台 | 层级 | 路径 | |------|------|------| -| **Linux** | System | `/etc/cfdesktop/system.ini` | +| **Linux** | System | `<应用目录>/config/system.ini` | | | User | `~/.config/cfdesktop/user.ini` | | | App | `<应用目录>/config/app.ini` | | **Windows** | System | `HKEY_LOCAL_MACHINE\Software\CFDesktop` | @@ -621,7 +625,7 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); class DebugPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { - QString path = "/etc/cfdesktop/system.ini"; + QString path = QCoreApplication::applicationDirPath() + "/config/system.ini"; std::cout << "System path: " << path.toStdString() << std::endl; return path; } @@ -657,7 +661,7 @@ ConfigStore::instance().initialize(std::make_shared()); // 方法 2:直接检查文件是否存在 void check_config_files() { std::array paths = { - "/etc/cfdesktop/system.ini", + QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini", std::getenv("HOME") + "/.config/cfdesktop/user.ini"s, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini" }; @@ -763,7 +767,7 @@ void diagnose_config_files() { std::cout << "\n=== 配置文件诊断 ===" << std::endl; std::array, 3> files = { - {Layer::System, "/etc/cfdesktop/system.ini"}, + {Layer::System, QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini"}, {Layer::User, std::getenv("HOME") + "/.config/cfdesktop/user.ini"s}, {Layer::App, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini"} }; @@ -1196,13 +1200,15 @@ void ensure_config_directory(const std::string& path) { ensure_config_directory("~/.config/cfdesktop/user.ini"); ```text -#### 问题 2:System 层路径需要 root 权限 +#### 问题 2:System 层路径不可写 + +CFDesktop 使用应用自管理目录,System 层配置位于应用目录下(如 `{app_dir}/system.ini`),无需 root 权限。 ```bash -# System 层配置通常位于 /etc,需要 root 权限写入 -sudo touch /etc/cfdesktop/system.ini -sudo chown $USER:$USER /etc/cfdesktop/system.ini -# 或者将应用配置放在用户目录 +# 配置文件位于应用目录下,应用启动时自动创建 +# 例如: <应用目录>/config/system.ini +# 如果目录不存在,手动创建: +mkdir -p <应用目录>/config ```text #### 问题 3:INI 文件编码 diff --git a/document/HandBook/desktop/base/config_manager/04-architecture.md b/document/HandBook/desktop/base/config_manager/04-architecture.md index 96b983c60..f2856608a 100644 --- a/document/HandBook/desktop/base/config_manager/04-architecture.md +++ b/document/HandBook/desktop/base/config_manager/04-architecture.md @@ -28,7 +28,7 @@ ConfigStore 采用四层优先级架构,实现了配置的层次化管理和 | - 用户个性化设置,持久化存储 | +-----------------------------------------------+ | System Layer (最低优先级) | -| - 系统级配置,/etc/cfdesktop/system.ini | +| - 系统级配置,{app_dir}/system.ini (CFDesktop 自管理目录) | | - 全局默认配置,只读或需要特权写入 | +-----------------------------------------------+ ```cpp @@ -196,7 +196,7 @@ public: | 层级 | 路径格式 | 说明 | |------|----------------------------|--------------------------| -| System | `/etc/cfdesktop/system.ini` | 系统配置,需要权限写入 | +| System | `{app_dir}/system.ini` | 系统配置 (CFDesktop 自管理目录) | | User | `~/.config/cfdesktop/user.ini` | 用户配置,自动创建目录 | | App | `config/app.ini` | 相对路径,运行时可写 | | Temp | (内存) | 不持久化 | diff --git a/document/HandBook/desktop/base/config_manager/README.md b/document/HandBook/desktop/base/config_manager/README.md index 88cfefbe0..17f18c323 100644 --- a/document/HandBook/desktop/base/config_manager/README.md +++ b/document/HandBook/desktop/base/config_manager/README.md @@ -20,7 +20,7 @@ ConfigStore 采用四层优先级架构,支持配置的自然覆盖: | **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | | **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | | **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | -| **System** | 低 (0) | 读写 | `/etc/cfdesktop/system.ini` | 系统默认配置 | +| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | 查询时按优先级从高到低查找,写入时默认写入 App 层。 diff --git a/document/design_stage/01_phase1_hardware_probe.md b/document/design_stage/01_phase1_hardware_probe.md index f22d15ea3..75f2b6842 100644 --- a/document/design_stage/01_phase1_hardware_probe.md +++ b/document/design_stage/01_phase1_hardware_probe.md @@ -25,7 +25,7 @@ description: "Phase 1: 硬件探针与能力分级详细设计文档 的详细 - [ ] `HardwareProbe` 模块及其单元测试 - [ ] `CapabilityPolicy` 策略引擎 - [ ] `HWTier` 枚举定义及查询接口 -- [ ] 设备配置文件 `/etc/CFDesktop/device.conf` +- [ ] ~~设备配置文件 `/etc/CFDesktop/device.conf`~~ ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API + CFDesktop 自管理目录,不读取系统级路径 - [ ] Mock 数据集用于单元测试 --- @@ -503,6 +503,9 @@ namespace CFDesktop::Base { /** * @brief 设备配置文件读取器 * + * ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API,配置在 CFDesktop 自管理目录内。 + * 以下 `/etc/CFDesktop/` 路径仅为早期设计参考。 + * * 读取 /etc/CFDesktop/device.conf,允许用户手动覆盖硬件检测。 * * 配置文件格式: @@ -526,13 +529,13 @@ public: /** * @brief 加载配置文件 */ - static Config load(const QString& path = "/etc/CFDesktop/device.conf"); + static Config load(const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时:实际使用应用内目录 /** * @brief 保存配置文件 */ static bool save(const Config& config, - const QString& path = "/etc/CFDesktop/device.conf"); + const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时 /** * @brief 执行自定义检测脚本 @@ -805,13 +808,15 @@ void HardwareProbe::calculateTier(HardwareInfo& info) { ## 六、设备配置文件格式 +> ⚠️ **过时说明**: 以下 `/etc/CFDesktop/` 路径为早期设计。实际实现中,CFDesktop 不读取系统级路径,设备配置通过 `setDeviceConfigOverride()` API 或 ConfigStore 应用内目录管理。 + ### 6.1 配置文件模板 **文件**: `configs/device.conf.template` ```ini # CFDesktop 设备配置文件 -# 部署位置: /etc/CFDesktop/device.conf +# 部署位置: CFDesktop 自管理目录内 (非 /etc/) [Device] # 档位设置: auto(自动检测) | low | mid | high diff --git a/document/design_stage/02_phase2_base_library.md b/document/design_stage/02_phase2_base_library.md index f1a4b38d4..117f80057 100644 --- a/document/design_stage/02_phase2_base_library.md +++ b/document/design_stage/02_phase2_base_library.md @@ -753,7 +753,7 @@ namespace CFDesktop::Base { * @brief 配置类型 */ enum class ConfigType { - System, // 系统配置 (/etc/CFDesktop/config.conf) + System, // ⚠️ 过时:实际使用 CFDesktop 自管理目录,非 /etc/ User, // 用户配置 (~/.config/CFDesktop/config.conf) App, // 应用配置 Temp // 临时配置 (内存) @@ -869,7 +869,7 @@ void setConfig(const QString& key, const T& value) { ### 6.2 配置文件格式 -**系统配置**: `/etc/CFDesktop/config.conf` +**系统配置**: ~~`/etc/CFDesktop/config.conf`~~ ⚠️ 过时:实际使用 CFDesktop 自管理目录 ```ini [General] diff --git a/document/todo/desktop/07_render_backend.md b/document/todo/desktop/07_render_backend.md index eb57e52d4..49067ebd9 100644 --- a/document/todo/desktop/07_render_backend.md +++ b/document/todo/desktop/07_render_backend.md @@ -66,7 +66,7 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 - [ ] Mid Tier: 软件降级可选 - [ ] Low Tier: LinuxFB 直连 - [ ] 实现屏幕尺寸配置 - - [ ] 读取 `/etc/CFDesktop/display.conf` + - [ ] 读取 `{desktop_root}/config/display.conf` - [ ] 命令行参数 `--screen-size=WxH` - [ ] 环境变量 `CFDESKTOP_SCREEN_SIZE` - [ ] 实现 DRM 设备选择 @@ -174,7 +174,7 @@ description: "状态: 🚧 部分完成 (接口已实现,具体后端待开发 - [ ] `src/desktop/render/screen_simulator.cpp` ### 配置文件 -- [ ] `/etc/CFDesktop/display.conf` (Linux) +- [ ] `{desktop_root}/config/display.conf` (Linux) - [ ] `configs/display.conf` (Windows 开发) ### 测试文件 diff --git a/document/todo/done/01_hardware_probe_status.md b/document/todo/done/01_hardware_probe_status.md index bebe0e676..7189c1436 100644 --- a/document/todo/done/01_hardware_probe_status.md +++ b/document/todo/done/01_hardware_probe_status.md @@ -6,9 +6,9 @@ description: "模块ID: Phase 1,状态: 🚧 部分完成" # Phase 1: 硬件探针与能力分级 - 状态文档 > **模块ID**: Phase 1 -> **状态**: 🚧 部分完成 -> **总体进度**: ~90% -> **最后更新**: 2026-03-27 +> **状态**: ✅ 完成 +> **总体进度**: 100% +> **最后更新**: 2026-05-23 --- @@ -41,9 +41,9 @@ description: "模块ID: Phase 1,状态: 🚧 部分完成" | MemoryDetector | 95% | ✅ 完成 | | GPUDetector | 90% | ✅ 核心功能已完成 | | NetworkDetector | 85% | ✅ 核心功能已完成 | -| HWTier 系统 | 0% | ❌ 待实现 | -| CapabilityPolicy | 0% | ❌ 待实现 | -| HardwareProbe 主类 | 0% | ❌ 待实现 | +| HWTier 系统 | 100% | ✅ 完成 | +| CapabilityPolicy | 100% | ✅ 完成 | +| HardwareProbe 主类 | 100% | ✅ 完成 | --- @@ -187,181 +187,80 @@ if (mem_info) { --- -## 五、待完成模块 +## 五、HWTier 系统 (Phase 1 核心功能) — ✅ 已完成 -> **注意**: GPU 和 Network 检测器已完成,请参考上方"三、已完成工作"中的 3.3 和 3.4 节。 +> **注意**: GPU 和 Network 检测器已完成,HWTier 系统已实现,Phase 1 核心功能全部完成。 -### 5.1 HWTier 枚举定义 (P0) +### 5.1 HWTier 枚举定义 ✅ -**需求描述**: -- 定义 Low/Mid/High 三档枚举 -- 每档对应硬件配置说明 -- 档位字符串转换函数 +**实现位置**: `base/include/system/hardware_tier/hardware_tier_data.h` -**建议文件路径**: `base/hardware/HWTier.h` +- [x] `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) +- [x] 档位字符串转换函数 `hardwareTierLevelToString()` +- [x] 各档位对应硬件配置说明 (i.MX6ULL / RK3568 / RK3588) -**接口设计**: -```cpp -namespace CFDesktop::Base { +### 5.2 硬件数据收集器 ✅ -enum class HWTier { - Low, // IMX6ULL 级别 - Mid, // RK3568 级别 - High // RK3588 级别 -}; +**实现位置**: `base/system/hardware_tier/hardware_tier_collector.h`, `base/system/hardware_tier/default/default_collector.cpp` -QString tierToString(HWTier tier); -HWTier tierFromString(const QString& str); +- [x] `IHardwareCollector` 接口 — 可插拔收集器 +- [x] `HardwareData` 结构体 — CPU/GPU/Memory/Display 原始数据 +- [x] 默认收集器 — 整合 CPU/GPU/Memory/Display 检测器 +- [x] 跨平台支持 (Linux/Windows) -} // namespace CFDesktop::Base -```yaml +### 5.3 评分引擎 ✅ ---- +**实现位置**: `base/system/hardware_tier/hardware_tier_scorer.h`, `base/system/hardware_tier/default/default_*_scorer.cpp` -### 5.2 HardwareProbe 主类 (P0) +- [x] `IHardwareScorer` 接口 — 可插拔评分器 +- [x] 维度评分类型: `CpuScore`, `GpuScore`, `MemoryScore`, `DisplayScore` (各 0-100) +- [x] 默认评分器: CPU/GPU/Memory/Display 四维度独立评分 +- [x] 可通过 `registerScorer()` 替换各维度评分逻辑 -**需求描述**: -- 整合所有检测器 -- 实现档位计算逻辑 -- 单例模式 -- Mock 数据支持 +### 5.4 档位评估器 ✅ -**建议文件路径**: -- `base/hardware/HardwareProbe.h` -- `base/hardware/HardwareProbe.cpp` +**实现位置**: `base/system/hardware_tier/hardware_tier_assessor.h`, `base/system/hardware_tier/default/default_assessor.cpp` -**接口设计**: -```cpp -namespace CFDesktop::Base { - -struct HardwareInfo { - HWTier tier = HWTier::Low; - CPUInfo cpu; - GPUInfo gpu; - MemoryInfo memory; - QList networkInterfaces; - QString deviceTreeCompatible; - bool isUserOverridden = false; -}; - -class HardwareProbe : public QObject { - Q_OBJECT +- [x] `IHardwareAssessor` 接口 — 可插拔评估器 +- [x] `HardwareTierAssessment` 结构体 — 四维度评分 + 总档位 + 覆盖信息 +- [x] 默认评估器 — 综合四维度分数计算档位 -public: - static HardwareProbe* instance(); - HardwareInfo probe(); - HardwareInfo forceRedetect(); - HWTier currentTier() const; - const HardwareInfo& hardwareInfo() const; - void setMockData(const HardwareInfo& mockInfo); - -signals: - void probeCompleted(const HardwareInfo& info); - void tierChanged(HWTier newTier); - -private: - HardwareProbe(QObject* parent = nullptr); - void calculateTier(HardwareInfo& info); -}; +### 5.5 策略引擎 ✅ -} // namespace CFDesktop::Base -```yaml +**实现位置**: `base/system/hardware_tier/hardware_tier_policy.h`, `base/system/hardware_tier/default/default_policy.cpp` ---- +- [x] `IHardwarePolicy` 接口 — 可插拔策略 +- [x] `HardwareTierCapabilities` 结构体 — 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) +- [x] 默认策略 — 根据 Low/Mid/High 档位映射能力标志 +- [x] 部分动画支持 (`enable_partial_animation`) -### 5.3 CapabilityPolicy 策略引擎 (P0) +### 5.6 DeviceConfig 覆盖 ✅ -**需求描述**: -- 定义各策略结构体 -- 实现档位特定策略配置 -- 策略查询接口 +**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` -**建议文件路径**: -- `base/hardware/CapabilityPolicy.h` -- `base/hardware/CapabilityPolicy.cpp` +- [x] `setDeviceConfigOverride()` — 手动强制档位 +- [x] `clearDeviceConfigOverride()` — 清除覆盖 +- [x] 覆盖时跳过收集/评分,直接使用指定档位 +- [x] 覆盖原因记录 (`override_reason`) -**接口设计**: -```cpp -namespace CFDesktop::Base { +### 5.7 管线注册 API ✅ -struct AnimationPolicy { - bool enabled = false; - int defaultDurationMs = 0; - int maxConcurrentAnimations = 0; - bool allowComplexEffects = false; -}; +**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` -struct RenderingPolicy { - QString qtPlatform; - bool useOpenGL = false; - QString openGLVersion; - bool useVSync = false; - int maxFPS = 60; -}; +- [x] `registerCollector()` — 注册自定义收集器 +- [x] `registerScorer()` — 注册自定义评分器 (按维度) +- [x] `registerAssessor()` — 注册自定义评估器 +- [x] `registerPolicy()` — 注册自定义策略 +- [x] `assessHardware()` — 执行完整管线 (缓存支持) +- [x] `getHardwareTierCapabilities()` — 查询能力标志 -struct VideoDecoderPolicy { - bool useHardwareDecoder = false; - QStringList supportedCodecs; - int maxResolution = 0; - int maxBitrate = 0; -}; +### 5.8 示例代码 ✅ -struct MemoryPolicy { - int maxImageCacheBytes = 0; - int maxFontCacheBytes = 0; - bool enableTextureCompression = false; - int maxWindowSurfaces = 0; -}; +**实现位置**: `example/base/system/example_hardware_tier.cpp` -class CapabilityPolicy : public QObject { - Q_OBJECT - -public: - static CapabilityPolicy* instance(); - AnimationPolicy getAnimationPolicy() const; - RenderingPolicy getRenderingPolicy() const; - VideoDecoderPolicy getVideoDecoderPolicy() const; - MemoryPolicy getMemoryPolicy() const; - HWTier currentTier() const; - void overrideTier(HWTier tier); - -signals: - void policyChanged(HWTier newTier); -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -### 5.4 DeviceConfig 配置文件 (P1) - -**需求描述**: -- INI 格式配置文件解析 -- 支持档位手动覆盖 -- 自定义检测脚本执行 - -**建议文件路径**: -- `base/hardware/DeviceConfig.h` -- `base/hardware/DeviceConfig.cpp` - -**配置文件格式** (`/etc/CFDesktop/device.conf`): -```ini -[Device] -Tier=auto|low|mid|high -CustomScript=/opt/cfdesktop/detect-hardware.sh -BoardName=Generic-Board - -[Overrides] -EnableAnimations=true -AnimationDuration=200 -RenderingBackend=auto -ForceOpenGL=false -VideoDecoder=auto - -[Logging] -LogLevel=Info -```yaml +- [x] 完整的硬件分级评估示例 +- [x] 四维度评分展示 +- [x] 能力标志展示 --- @@ -478,72 +377,47 @@ base/ ### 待创建文件 +> HWTier 系统已全部实现,无待创建文件。 + ```text base/ -├── hardware/ -│ ├── HWTier.h # 档位枚举 -│ ├── HardwareInfo.h # 硬件信息结构体 -│ ├── HardwareProbe.h # 探针主类 -│ ├── CapabilityPolicy.h # 策略引擎 -│ ├── DeviceConfig.h # 配置文件 -│ ├── detectors/ -│ │ ├── GPUDetector.h # GPU 检测器 -│ │ └── NetworkDetector.h # 网络检测器 -│ └── platform/ -│ ├── LinuxDetector.cpp # Linux 平台实现 -│ └── WindowsDetector.cpp # Windows 平台实现 +├── include/system/hardware_tier/ +│ ├── hardware_tier.h # 公共 API (管线注册 + 评估入口) +│ └── hardware_tier_data.h # 数据结构 (枚举/评分/结果/能力标志) +├── system/hardware_tier/ +│ ├── hardware_tier_collector.h # IHardwareCollector 接口 +│ ├── hardware_tier_scorer.h # IHardwareScorer 接口 +│ ├── hardware_tier_assessor.h # IHardwareAssessor 接口 +│ ├── hardware_tier_policy.h # IHardwarePolicy 接口 +│ ├── default_factories.h # 默认工厂函数 +│ ├── hardware_tier.cpp # 管线实现 +│ └── default/ +│ ├── default_collector.cpp # 默认收集器 +│ ├── default_cpu_scorer.cpp # 默认 CPU 评分 +│ ├── default_gpu_scorer.cpp # 默认 GPU 评分 +│ ├── default_memory_scorer.cpp # 默认 Memory 评分 +│ ├── default_display_scorer.cpp # 默认 Display 评分 +│ ├── default_assessor.cpp # 默认评估器 +│ └── default_policy.cpp # 默认策略 ```text -### 测试文件 - -```text -tests/ -├── hardware/ -│ ├── test_hardware_probe.cpp # 主测试 -│ ├── test_capability_policy.cpp # 策略测试 -│ └── mock/ -│ └── proc/ # Mock 数据 -```yaml - --- ## 八、下一步行动建议 -### 优先级1 (高) - -1. **定义 HWTier 枚举** - - 优先级: 最高 - - 理由: 其他模块依赖此定义 - - 预计工作量: 0.5天 - -### 优先级2 (中) - -2. **实现 HardwareProbe 主类** - - 优先级: 中 - - 理由: 整合所有检测器 - - 预计工作量: 2-3天 - -3. **实现 CapabilityPolicy** - - 优先级: 中 - - 理由: 为上层提供策略配置 - - 预计工作量: 2-3天 - -4. **实现 DeviceConfig** - - 优先级: 中 - - 理由: 支持手动配置覆盖 - - 预计工作量: 1-2天 +### 已完成 ✅ -### 优先级3 (低) +1. **HWTier 枚举定义** — ✅ 已实现 (`HardwareTierLevel`) +2. **HardwareProbe 管线** — ✅ 已实现 (可插拔 Collect→Score→Assess→Policy 四阶段管线) +3. **CapabilityPolicy 策略引擎** — ✅ 已实现 (`IHardwarePolicy` + 默认策略) +4. **DeviceConfig 覆盖** — ✅ 已实现 (`setDeviceConfigOverride()`) -5. **创建 Mock 数据集** - - 优先级: 低 - - 理由: 单元测试需要 - - 预计工作量: 1天 +### 后续可选增强 -6. **编写单元测试** - - 优先级: 低 - - 理由: 确保代码质量 - - 预计工作量: 3-4天 +1. **集成 ConfigStore 查询覆盖** — 从 ConfigStore 读取设备档位配置,自动调用 `setDeviceConfigOverride()` +2. **自定义检测脚本执行** — 支持外部脚本注入硬件数据 +3. **Mock 数据集** — 单元测试用模拟数据 +4. **性能测试** — 各评分器在不同硬件下的基准测试 --- @@ -556,5 +430,6 @@ tests/ --- -*文档版本: v1.0* +*文档版本: v2.0* *生成时间: 2026-03-11* +*最后更新: 2026-05-23 (HWTier 系统完成)* diff --git a/document/todo/done/02_base_library_status.md b/document/todo/done/02_base_library_status.md index 887ebe886..6ec37edf5 100644 --- a/document/todo/done/02_base_library_status.md +++ b/document/todo/done/02_base_library_status.md @@ -462,7 +462,7 @@ flush(); |------|--------|------|------| | QSSProcessor | P2 | - | 待规划 | | VariableResolver | P2 | - | 待规划 | -| HWTier 降级逻辑 | P1 | HWTier | 待规划 | +| HWTier 降级逻辑 | P1 | HWTier | ✅ 已实现 (hardware_tier_policy) | | 屏幕参数检测 | P2 | - | 待规划 | | 模拟器注入接口 | P2 | - | 待规划 | diff --git a/document/todo/done/06_infrastructure_status.md b/document/todo/done/06_infrastructure_status.md index bd1022bc9..6388e93b4 100644 --- a/document/todo/done/06_infrastructure_status.md +++ b/document/todo/done/06_infrastructure_status.md @@ -96,7 +96,7 @@ description: "状态: ✅ 部分完成,总体进度: ~50%" #### 配置文件路径 -- System: `/etc/cfdesktop/system.ini` +- System: `{app_dir}/system.ini`(CFDesktop 自管理目录内) - User: `~/.config/cfdesktop/user.ini` - App: `config/app.ini` diff --git a/document/todo/done/PROJECT_STATUS_REPORT.md b/document/todo/done/PROJECT_STATUS_REPORT.md index 0c7ed5771..7549d2908 100644 --- a/document/todo/done/PROJECT_STATUS_REPORT.md +++ b/document/todo/done/PROJECT_STATUS_REPORT.md @@ -21,7 +21,7 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D | 模块 | 完成度 | 状态 | 优先级 | |------|--------|------|--------| | Phase 0: 工程骨架 | 100% | ✅ 已完成并归档 | P0 | -| Phase 1: 硬件探针 | 90% | 🚧 进行中 | P0 | +| Phase 1: 硬件探针 | 100% | ✅ 完成 | P0 | | Phase 2: Base库核心 | 85% | 🚧 进行中 | P0 | | Phase 3: 输入抽象层 | 0% | ⬜ 待开始 | P1 | | Phase 4: 多平台模拟器 | 0% | ⬜ 待开始 | P1 | @@ -59,7 +59,7 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D --- -### 1.1 硬件探针 (Phase 1) - 90% 🚧 进行中 +### 1.1 硬件探针 (Phase 1) - 100% ✅ 完成 #### ✅ 已完成 - **CPU 检测器** (100%) - `base/system/cpu/` 模块 @@ -99,11 +99,19 @@ CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material D - **网络检测器** (85%) - `base/system/network/` 模块 - 网卡枚举、IP地址、可达性检测 +#### ✅ 已完成 (HWTier 系统) +- **HWTier 系统** (100%) - `base/system/hardware_tier/` 模块 + - `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) + - 可插拔四阶段管线 (Collect→Score→Assess→Policy) + - 默认收集器、四维度评分器 (CPU/GPU/Memory/Display 各 0-100) + - 默认评估器和策略引擎 + - DeviceConfig 覆盖支持 (`setDeviceConfigOverride()`) + - `HardwareTierCapabilities` 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) + - 完整管线注册 API (`registerCollector/Scorer/Assessor/Policy`) + - 示例程序 `example/base/system/example_hardware_tier.cpp` + #### ❌ 待完成 -- HWTier 枚举定义 (Low/Mid/High 三档) -- HardwareProbe 主类整合 -- CapabilityPolicy 策略引擎 -- DeviceConfig 配置文件系统 +- 自定义检测脚本执行支持 --- @@ -472,6 +480,7 @@ CFDesktop/ │ ├── system/memory/ # ✅ 内存检测 (95%) │ ├── system/gpu/ # ✅ GPU检测 (90%) │ ├── system/network/ # ✅ 网络检测 (85%) +│ ├── system/hardware_tier/ # ✅ HWTier 硬件分级 (100%) │ ├── device/console/ # ✅ 控制台设备 (85%) │ └── include/base/policy_chain/ # ✅ 策略链 (80%) ├── ui/ # UI框架 (95%) diff --git a/test/ui/core/CMakeLists.txt b/test/ui/core/CMakeLists.txt index 00392a7cb..f4cec64e4 100644 --- a/test/ui/core/CMakeLists.txt +++ b/test/ui/core/CMakeLists.txt @@ -1,12 +1 @@ -log_info("UI_Core_Tests" "Configured ui/core tests") - -# ============================================================================= -# ui/core tests - token_test -# ============================================================================= -add_gtest_executable( - TEST_NAME token_test - SOURCE_FILE token_test.cpp - LINK_LIBRARIES Qt6::Core;Qt6::Gui;GTest::gtest;GTest::gtest_main;cfui;CFDesktop::base_headers - LABELS "ui;core;token" - LOG_MODULE ui_core_tests -) +log_info("UI_Core_Tests" "Configured ui/core tests") diff --git a/test/ui/core/token_test.cpp b/test/ui/core/token_test.cpp deleted file mode 100644 index e15f66a0d..000000000 --- a/test/ui/core/token_test.cpp +++ /dev/null @@ -1,579 +0,0 @@ -/** - * @file token_test.cpp - * @brief Comprehensive unit tests for cf::ui::core Token system using GoogleTest - * - * Test Coverage: - * 1. TokenRegistry singleton - * 2. StaticToken - compile-time type-safe tokens - * 3. DynamicToken - runtime type-erased tokens - * 4. Thread safety - concurrent access - * 5. Error handling - TokenError types - * 6. Complex types - */ - -#include "ui/core/token.hpp" -#include -#include -#include -#include -#include - -using namespace cf::ui::core; -// Bring _hash literal operator into scope -using namespace cf::hash; - -// ============================================================================= -// Test Suite 1: TokenRegistry Singleton -// ============================================================================= - -TEST(TokenRegistryTest, Singleton) { - auto& r1 = TokenRegistry::get(); - auto& r2 = TokenRegistry::get(); - - EXPECT_EQ(&r1, &r2); -} - -// ============================================================================= -// Test Suite 2: StaticToken -// ============================================================================= - -TEST(StaticTokenTest, RegisterAndGet) { - using TestToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up if exists - registry.remove("testToken"_hash); - - auto result = registry.register_token(42); - EXPECT_TRUE(result) << "Registration should succeed"; - - auto get_result = TestToken::get(); - EXPECT_TRUE(get_result) << "Get should succeed"; - EXPECT_EQ(**get_result, 42); - - // Cleanup - registry.remove("testToken"_hash); -} - -TEST(StaticTokenTest, MultipleTypes) { - using IntToken = StaticToken; - using StringToken = StaticToken; - using DoubleToken = StaticToken; - - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("intToken"_hash); - registry.remove("stringToken"_hash); - registry.remove("doubleToken"_hash); - - EXPECT_TRUE(registry.register_token(123)); - EXPECT_TRUE(registry.register_token("hello")); - EXPECT_TRUE(registry.register_token(3.14)); - - auto int_result = IntToken::get(); - auto string_result = StringToken::get(); - auto double_result = DoubleToken::get(); - - EXPECT_TRUE(int_result); - EXPECT_TRUE(string_result); - EXPECT_TRUE(double_result); - - EXPECT_EQ(**int_result, 123); - EXPECT_EQ(**string_result, "hello"); - EXPECT_DOUBLE_EQ(**double_result, 3.14); - - // Cleanup - registry.remove("intToken"_hash); - registry.remove("stringToken"_hash); - registry.remove("doubleToken"_hash); -} - -TEST(StaticTokenTest, NotFound) { - using NonExistentToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Make sure it doesn't exist - registry.remove("nonExistentToken"_hash); - - auto result = NonExistentToken::get(); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::NotFound); - } -} - -TEST(StaticTokenTest, ConstAccess) { - using ConstTestToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("constTestToken"_hash); - - EXPECT_TRUE(registry.register_token(999)); - - auto result = ConstTestToken::get_const(); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 999); - - // Cleanup - registry.remove("constTestToken"_hash); -} - -TEST(StaticTokenTest, AlreadyRegistered) { - using DuplicateToken = StaticToken; - auto& registry = TokenRegistry::get(); - - // Clean up - registry.remove("duplicateToken"_hash); - - EXPECT_TRUE(registry.register_token(1)); - auto result2 = registry.register_token(2); - EXPECT_FALSE(result2); - if (!result2) { - EXPECT_EQ(result2.error().kind, TokenError::Kind::AlreadyRegistered); - } - - // First value should still be there - auto get_result = DuplicateToken::get(); - EXPECT_TRUE(get_result); - EXPECT_EQ(**get_result, 1); - - // Cleanup - registry.remove("duplicateToken"_hash); -} - -// ============================================================================= -// Test Suite 3: DynamicToken -// ============================================================================= - -TEST(DynamicTokenTest, RegisterAndGet) { - auto& registry = TokenRegistry::get(); - - std::string name = "dynamicTest"; - registry.remove(name); - - auto reg_result = registry.register_dynamic(name, 42); - EXPECT_TRUE(reg_result); - - auto get_result = registry.get_dynamic(name); - EXPECT_TRUE(get_result); - EXPECT_EQ(**get_result, 42); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, MultipleTypes) { - auto& registry = TokenRegistry::get(); - - std::string name1 = "dynamicInt"; - std::string name2 = "dynamicString"; - std::string name3 = "dynamicVector"; - - registry.remove(name1); - registry.remove(name2); - registry.remove(name3); - - EXPECT_TRUE(registry.register_dynamic(name1, 456)); - EXPECT_TRUE(registry.register_dynamic(name2, "world")); - EXPECT_TRUE(registry.register_dynamic>(name3, {1, 2, 3})); - - auto r1 = registry.get_dynamic(name1); - auto r2 = registry.get_dynamic(name2); - auto r3 = registry.get_dynamic>(name3); - - EXPECT_TRUE(r1); - EXPECT_TRUE(r2); - EXPECT_TRUE(r3); - - EXPECT_EQ(**r1, 456); - EXPECT_EQ(**r2, "world"); - EXPECT_EQ((*r3)->size(), 3); - EXPECT_EQ((**r3)[0], 1); - EXPECT_EQ((**r3)[1], 2); - EXPECT_EQ((**r3)[2], 3); - - // Cleanup - registry.remove(name1); - registry.remove(name2); - registry.remove(name3); -} - -TEST(DynamicTokenTest, TypeMismatch) { - auto& registry = TokenRegistry::get(); - - std::string name = "typeMismatch"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 42)); - - // Try to get as wrong type - auto result = registry.get_dynamic(name); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::TypeMismatch); - } - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, NotFoundDynamic) { - auto& registry = TokenRegistry::get(); - - std::string name = "nonExistentDynamic"; - registry.remove(name); - - auto result = registry.get_dynamic(name); - EXPECT_FALSE(result); - if (!result) { - EXPECT_EQ(result.error().kind, TokenError::Kind::NotFound); - } -} - -TEST(DynamicTokenTest, RegisterByCopy) { - auto& registry = TokenRegistry::get(); - - std::string name = "copyTest"; - registry.remove(name); - - std::string value = "copied value"; - EXPECT_TRUE(registry.register_dynamic(name, value)); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, "copied value"); - EXPECT_EQ(value, "copied value"); // Original unchanged - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, RegisterByMove) { - auto& registry = TokenRegistry::get(); - - std::string name = "moveTest"; - registry.remove(name); - - std::string value = "moved value"; - EXPECT_TRUE(registry.register_dynamic(name, std::move(value))); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, "moved value"); - // Original may be in moved-from state - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, ConstAccess) { - auto& registry = TokenRegistry::get(); - - std::string name = "constDynamic"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 789)); - - auto result = registry.get_dynamic_const(name); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 789); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, Contains) { - auto& registry = TokenRegistry::get(); - - std::string name = "containsTest"; - registry.remove(name); - - EXPECT_FALSE(registry.contains(name)); - - EXPECT_TRUE(registry.register_dynamic(name, 1)); - EXPECT_TRUE(registry.contains(name)); - - // Cleanup - registry.remove(name); -} - -TEST(DynamicTokenTest, Remove) { - auto& registry = TokenRegistry::get(); - - std::string name = "removeTest"; - registry.remove(name); - - EXPECT_TRUE(registry.register_dynamic(name, 1)); - EXPECT_TRUE(registry.contains(name)); - - EXPECT_TRUE(registry.remove(name)); - EXPECT_FALSE(registry.contains(name)); - - EXPECT_FALSE(registry.remove(name)); // Already removed -} - -TEST(DynamicTokenTest, GetByHash) { - auto& registry = TokenRegistry::get(); - - std::string name = "hashLookup"; - constexpr uint64_t hash = "hashLookup"_hash; - registry.remove(hash); - - EXPECT_TRUE(registry.register_dynamic(name, 111)); - - auto result = registry.get_dynamic_by_hash(hash); - EXPECT_TRUE(result); - EXPECT_EQ(**result, 111); - - // Cleanup - registry.remove(hash); -} - -// ============================================================================= -// Test Suite 4: Complex Types -// ============================================================================= - -// Note: std::any requires copy-constructible types, so unique_ptr and atomic -// cannot be directly stored. Use shared_ptr or regular types instead. - -TEST(ComplexTypesTest, SharedPtr) { - using PtrToken = StaticToken, "ptrToken"_hash>; - auto& registry = TokenRegistry::get(); - - registry.remove("ptrToken"_hash); - - EXPECT_TRUE(registry.register_token(std::make_shared(333))); - - auto result = PtrToken::get(); - EXPECT_TRUE(result); - - auto ptr = **result; - EXPECT_EQ(*ptr.get(), 333); - - // Cleanup - registry.remove("ptrToken"_hash); -} - -TEST(ComplexTypesTest, Vector) { - auto& registry = TokenRegistry::get(); - - std::string name = "vectorToken"; - registry.remove(name); - - std::vector vec = {10, 20, 30, 40, 50}; - EXPECT_TRUE(registry.register_dynamic>(name, std::move(vec))); - - auto result = registry.get_dynamic>(name); - EXPECT_TRUE(result); - EXPECT_EQ((*result)->size(), 5); - EXPECT_EQ((**result)[0], 10); - EXPECT_EQ((**result)[4], 50); - - // Cleanup - registry.remove(name); -} - -// Custom struct for testing -struct TestStruct { - int a; - double b; - std::string c; - - bool operator==(const TestStruct& other) const { - return a == other.a && b == other.b && c == other.c; - } -}; - -TEST(ComplexTypesTest, CustomStruct) { - auto& registry = TokenRegistry::get(); - - std::string name = "structToken"; - registry.remove(name); - - TestStruct s{123, 4.56, "test"}; - EXPECT_TRUE(registry.register_dynamic(name, s)); - - auto result = registry.get_dynamic(name); - EXPECT_TRUE(result); - EXPECT_EQ((*result)->a, 123); - EXPECT_DOUBLE_EQ((*result)->b, 4.56); - EXPECT_EQ((*result)->c, "test"); - - // Cleanup - registry.remove(name); -} - -// ============================================================================= -// Test Suite 5: Thread Safety -// ============================================================================= - -// Note: std::atomic is not copy-constructible and cannot be stored in std::any. -// We use regular int and rely on TokenRegistry's internal mutex for safety. -TEST(ThreadSafetyTest, ConcurrentReads) { - using SharedToken = StaticToken; - auto& registry = TokenRegistry::get(); - - registry.remove("sharedToken"_hash); - - EXPECT_TRUE(registry.register_token(0)); - - constexpr int num_threads = 10; - constexpr int reads_per_thread = 100; - std::vector threads; - - for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&]() { - for (int j = 0; j < reads_per_thread; ++j) { - auto result = SharedToken::get(); - if (result) { - // Just verify we can read the value - EXPECT_GE(**result, 0); - } - } - }); - } - - for (auto& t : threads) { - t.join(); - } - - auto final_result = SharedToken::get(); - EXPECT_TRUE(final_result); - EXPECT_EQ(**final_result, 0); - - // Cleanup - registry.remove("sharedToken"_hash); -} - -TEST(ThreadSafetyTest, ConcurrentWrites) { - auto& registry = TokenRegistry::get(); - std::atomic success_count{0}; - std::atomic failure_count{0}; - - constexpr int num_threads = 10; - std::vector threads; - - for (int i = 0; i < num_threads; ++i) { - threads.emplace_back([&, i]() { - std::string name = "concurrent_" + std::to_string(i); - auto result = registry.register_dynamic(name, i); - if (result) { - success_count++; - } else { - failure_count++; - } - }); - } - - for (auto& t : threads) { - t.join(); - } - - // All registrations should succeed since names are different - EXPECT_EQ(success_count, num_threads); - EXPECT_EQ(failure_count, 0); - - // Cleanup - for (int i = 0; i < num_threads; ++i) { - std::string name = "concurrent_" + std::to_string(i); - registry.remove(name); - } -} - -TEST(ThreadSafetyTest, ReadDuringWrite) { - using ReadWriteToken = StaticToken; - auto& registry = TokenRegistry::get(); - - registry.remove("readWriteToken"_hash); - EXPECT_TRUE(registry.register_token(100)); - - std::atomic stop{false}; - std::atomic read_count{0}; - std::atomic write_count{0}; - - // Reader threads - std::vector readers; - for (int i = 0; i < 5; ++i) { - readers.emplace_back([&]() { - while (!stop) { - auto result = ReadWriteToken::get(); - if (result) { - read_count++; - } - } - }); - } - - // Writer thread - std::thread writer([&]() { - for (int i = 0; i < 100; ++i) { - // Re-register with new value - registry.remove("readWriteToken"_hash); - registry.register_token(100 + i); - write_count++; - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - }); - - writer.join(); - stop = true; - - for (auto& t : readers) { - t.join(); - } - - EXPECT_GT(read_count, 0); - EXPECT_EQ(write_count, 100); - - // Cleanup - registry.remove("readWriteToken"_hash); -} - -// ============================================================================= -// Test Suite 6: Size and Cleanup -// ============================================================================= - -TEST(TokenRegistryTest, SizeTracking) { - auto& registry = TokenRegistry::get(); - - size_t initial_size = registry.size(); - - std::string name1 = "sizeTest1"; - std::string name2 = "sizeTest2"; - - registry.remove(name1); - registry.remove(name2); - - EXPECT_TRUE(registry.register_dynamic(name1, 1)); - EXPECT_EQ(registry.size(), initial_size + 1); - - EXPECT_TRUE(registry.register_dynamic(name2, 2)); - EXPECT_EQ(registry.size(), initial_size + 2); - - registry.remove(name1); - EXPECT_EQ(registry.size(), initial_size + 1); - - registry.remove(name2); - EXPECT_EQ(registry.size(), initial_size); -} - -TEST(TokenRegistryTest, RemoveNonExistent) { - auto& registry = TokenRegistry::get(); - - std::string name = "doesNotExist"; - registry.remove(name); - - EXPECT_FALSE(registry.remove(name)); - EXPECT_FALSE(registry.remove("anotherNonExistent"_hash)); -} - -// ============================================================================= -// Main -// ============================================================================= - -int main(int argc, char* argv[]) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/ui/core/CMakeLists.txt b/ui/core/CMakeLists.txt index c5688531c..57c7f3917 100644 --- a/ui/core/CMakeLists.txt +++ b/ui/core/CMakeLists.txt @@ -10,7 +10,7 @@ add_subdirectory(material) # ============================================================ # STATIC library for linking into cfui.dll -# Note: token.hpp and color_scheme.h are header-only files +# Note: color_scheme.h is a header-only interface file add_library(cf_ui_core STATIC theme_manager.cpp ) diff --git a/ui/core/material/cfmaterial_fonttype.cpp b/ui/core/material/cfmaterial_fonttype.cpp index 5e9c3bbfd..03da6a8d6 100644 --- a/ui/core/material/cfmaterial_fonttype.cpp +++ b/ui/core/material/cfmaterial_fonttype.cpp @@ -15,20 +15,6 @@ namespace cf::ui::core { namespace detail { -// ============================================================================= -// System Font Detection -// ============================================================================= - -/** - * @brief 获取系统默认无衬线字体 - * - * 根据平台返回合适的默认字体: - * - Windows: Segoe UI - * - macOS: .SF NS Text (San Francisco) - * - Linux: Ubuntu - * - * @return QString 系统默认字体名称 - */ inline QString systemDefaultFont() { #ifdef Q_OS_WIN return "Segoe UI"; @@ -39,149 +25,73 @@ inline QString systemDefaultFont() { #endif } -/** - * @brief 创建指定配置的字体 - * - * @param sizeSp 字体大小(sp 单位) - * @param weight 字重 (QFont::Weight) - * @param italic 是否斜体 - * @return QFont 配置好的字体对象 - */ inline QFont createFont(int sizeSp, QFont::Weight weight, bool italic = false) { QFont font(systemDefaultFont()); font.setStyleHint(QFont::SansSerif); font.setWeight(weight); font.setItalic(italic); - font.setPointSizeF(sizeSp); // 使用 pointSize (Qt 会处理 DPI) + font.setPointSizeF(sizeSp); return font; } -// ============================================================================= -// Material Design 3 Type Scale Registration -// ============================================================================= - -/** - * @brief 注册所有默认字体到注册表 - * - * 使用 Material Design 3 规范的字体大小、字重和行高。 - * - * @param registry 目标注册表 - */ -inline void registerDefaultFonts(EmbeddedTokenRegistry& registry) { - namespace literals = ::cf::ui::core::token::literals; - - // ========================================================================= - // Display Styles - 用于英雄内容 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_LARGE, - createFont(57, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_MEDIUM, - createFont(45, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_DISPLAY_SMALL, - createFont(36, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_LARGE, 64.0f); - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_MEDIUM, 52.0f); - registry.register_dynamic(literals::LINEHEIGHT_DISPLAY_SMALL, 44.0f); - - // ========================================================================= - // Headline Styles - 用于应用栏重要文本 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_LARGE, - createFont(32, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_MEDIUM, - createFont(28, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_HEADLINE_SMALL, - createFont(24, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_LARGE, 40.0f); - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_MEDIUM, 36.0f); - registry.register_dynamic(literals::LINEHEIGHT_HEADLINE_SMALL, 32.0f); - - // ========================================================================= - // Title Styles - 用于分区标题 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_LARGE, - createFont(22, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_MEDIUM, - createFont(16, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_TITLE_SMALL, - createFont(14, QFont::Medium)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_TITLE_LARGE, 28.0f); - registry.register_dynamic(literals::LINEHEIGHT_TITLE_MEDIUM, 24.0f); - registry.register_dynamic(literals::LINEHEIGHT_TITLE_SMALL, 20.0f); - - // ========================================================================= - // Body Styles - 用于主要内容 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_BODY_LARGE, - createFont(16, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_BODY_MEDIUM, - createFont(14, QFont::Normal)); - registry.register_dynamic(literals::TYPOGRAPHY_BODY_SMALL, - createFont(12, QFont::Normal)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_BODY_LARGE, 24.0f); - registry.register_dynamic(literals::LINEHEIGHT_BODY_MEDIUM, 20.0f); - registry.register_dynamic(literals::LINEHEIGHT_BODY_SMALL, 16.0f); - - // ========================================================================= - // Label Styles - 用于次要信息 - // ========================================================================= - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_LARGE, - createFont(14, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_MEDIUM, - createFont(12, QFont::Medium)); - registry.register_dynamic(literals::TYPOGRAPHY_LABEL_SMALL, - createFont(11, QFont::Medium)); - - // 行高 - registry.register_dynamic(literals::LINEHEIGHT_LABEL_LARGE, 20.0f); - registry.register_dynamic(literals::LINEHEIGHT_LABEL_MEDIUM, 16.0f); - registry.register_dynamic(literals::LINEHEIGHT_LABEL_SMALL, 16.0f); -} - } // namespace detail -// ============================================================================= -// MaterialTypography Implementation -// ============================================================================= - MaterialTypography::MaterialTypography() { - font_cache_.reserve(15); - line_height_cache_.reserve(15); - - // 注册所有默认字体 - detail::registerDefaultFonts(registry_); + fonts_.reserve(15); + line_heights_.reserve(15); + + // Display Styles + fonts_["md.typography.displayLarge"] = detail::createFont(57, QFont::Normal); + fonts_["md.typography.displayMedium"] = detail::createFont(45, QFont::Normal); + fonts_["md.typography.displaySmall"] = detail::createFont(36, QFont::Normal); + line_heights_["md.lineHeight.displayLarge"] = 64.0f; + line_heights_["md.lineHeight.displayMedium"] = 52.0f; + line_heights_["md.lineHeight.displaySmall"] = 44.0f; + + // Headline Styles + fonts_["md.typography.headlineLarge"] = detail::createFont(32, QFont::Normal); + fonts_["md.typography.headlineMedium"] = detail::createFont(28, QFont::Normal); + fonts_["md.typography.headlineSmall"] = detail::createFont(24, QFont::Normal); + line_heights_["md.lineHeight.headlineLarge"] = 40.0f; + line_heights_["md.lineHeight.headlineMedium"] = 36.0f; + line_heights_["md.lineHeight.headlineSmall"] = 32.0f; + + // Title Styles + fonts_["md.typography.titleLarge"] = detail::createFont(22, QFont::Medium); + fonts_["md.typography.titleMedium"] = detail::createFont(16, QFont::Medium); + fonts_["md.typography.titleSmall"] = detail::createFont(14, QFont::Medium); + line_heights_["md.lineHeight.titleLarge"] = 28.0f; + line_heights_["md.lineHeight.titleMedium"] = 24.0f; + line_heights_["md.lineHeight.titleSmall"] = 20.0f; + + // Body Styles + fonts_["md.typography.bodyLarge"] = detail::createFont(16, QFont::Normal); + fonts_["md.typography.bodyMedium"] = detail::createFont(14, QFont::Normal); + fonts_["md.typography.bodySmall"] = detail::createFont(12, QFont::Normal); + line_heights_["md.lineHeight.bodyLarge"] = 24.0f; + line_heights_["md.lineHeight.bodyMedium"] = 20.0f; + line_heights_["md.lineHeight.bodySmall"] = 16.0f; + + // Label Styles + fonts_["md.typography.labelLarge"] = detail::createFont(14, QFont::Medium); + fonts_["md.typography.labelMedium"] = detail::createFont(12, QFont::Medium); + fonts_["md.typography.labelSmall"] = detail::createFont(11, QFont::Medium); + line_heights_["md.lineHeight.labelLarge"] = 20.0f; + line_heights_["md.lineHeight.labelMedium"] = 16.0f; + line_heights_["md.lineHeight.labelSmall"] = 16.0f; } QFont MaterialTypography::queryTargetFont(const char* name) { - // 检查缓存 - auto it = font_cache_.find(name); - if (it != font_cache_.end()) { + auto it = fonts_.find(name); + if (it != fonts_.end()) { return it->second; } - - // 从注册表获取 - auto result = registry_.get_dynamic(name); - if (result && *result) { - font_cache_[name] = **result; - return **result; - } - - // 回退到默认字体 QFont fallback(detail::systemDefaultFont()); fallback.setPointSizeF(14); return fallback; } float MaterialTypography::getLineHeight(const char* styleName) const { - // 将 md.typography.xxx 转换为 md.lineHeight.xxx std::string key = styleName; const std::string prefix = "md.typography."; const std::string lineHeightPrefix = "md.lineHeight."; @@ -191,20 +101,16 @@ float MaterialTypography::getLineHeight(const char* styleName) const { key.replace(0, prefix.length(), lineHeightPrefix); } - // 检查缓存 - auto it = line_height_cache_.find(key); - if (it != line_height_cache_.end()) { - return it->second; - } + auto it = line_heights_.find(key); + return it != line_heights_.end() ? it->second : 0.0f; +} - // 从注册表获取 - auto result = registry_.get_dynamic_const(key.c_str()); - if (result && *result) { - line_height_cache_[key] = **result; - return **result; - } +void MaterialTypography::setFont(const std::string& name, const QFont& font) { + fonts_[name] = font; +} - return 0.0f; +void MaterialTypography::setLineHeight(const std::string& name, float lineHeight) { + line_heights_[name] = lineHeight; } } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_fonttype.h b/ui/core/material/cfmaterial_fonttype.h index 550fa1a28..e9d58e6d8 100644 --- a/ui/core/material/cfmaterial_fonttype.h +++ b/ui/core/material/cfmaterial_fonttype.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_fonttype.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Typography with EmbeddedTokenRegistry + * @brief Material Design 3 Typography * @version 0.1 * @date 2026-02-26 * @@ -9,7 +9,6 @@ * * @details * Implements the complete Material Design 3 Type Scale system with 15 styles. - * Fonts are stored in an embedded registry for independent typography instances. * * Font selection: * - Windows: Segoe UI (Chinese fallback to Microsoft YaHei UI) @@ -19,163 +18,19 @@ #pragma once -#include #include #include #include #include "../export.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "font_type.h" -#include "token.hpp" -#include "token/typography/cfmaterial_typography_token_literals.h" namespace cf::ui::core { -// ============================================================================= -// Typography Token Type Aliases - Material Typography System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Display tokens -using DisplayLargeToken = StaticToken; -using DisplayMediumToken = StaticToken; -using DisplaySmallToken = StaticToken; - -// Headline tokens -using HeadlineLargeToken = StaticToken; -using HeadlineMediumToken = StaticToken; -using HeadlineSmallToken = StaticToken; - -// Title tokens -using TitleLargeToken = StaticToken; -using TitleMediumToken = StaticToken; -using TitleSmallToken = StaticToken; - -// Body tokens -using BodyLargeToken = StaticToken; -using BodyMediumToken = StaticToken; -using BodySmallToken = StaticToken; - -// Label tokens -using LabelLargeToken = StaticToken; -using LabelMediumToken = StaticToken; -using LabelSmallToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Line Height Token Type Aliases -// ============================================================================= -namespace lineHeightTokens { -using namespace cf::ui::core::token::literals; - -using DisplayLargeLineHeight = StaticToken; -using DisplayMediumLineHeight = StaticToken; -using DisplaySmallLineHeight = StaticToken; - -using HeadlineLargeLineHeight = StaticToken; -using HeadlineMediumLineHeight = StaticToken; -using HeadlineSmallLineHeight = StaticToken; - -using TitleLargeLineHeight = StaticToken; -using TitleMediumLineHeight = StaticToken; -using TitleSmallLineHeight = StaticToken; - -using BodyLargeLineHeight = StaticToken; -using BodyMediumLineHeight = StaticToken; -using BodySmallLineHeight = StaticToken; - -using LabelLargeLineHeight = StaticToken; -using LabelMediumLineHeight = StaticToken; -using LabelSmallLineHeight = StaticToken; - -} // namespace lineHeightTokens - -// ============================================================================= -// Typography Group Structs - Material Typography System -// ============================================================================= - -/** - * @brief Display font styles group. - * - * Display styles are used for hero content with large emphasis. - * - * @since 0.1 - * @ingroup ui_core - */ -struct DisplayFonts { - tokens::DisplayLargeToken large; ///< 57sp, Regular, 64sp line-height - tokens::DisplayMediumToken medium; ///< 45sp, Regular, 52sp line-height - tokens::DisplaySmallToken small; ///< 36sp, Regular, 44sp line-height -}; - -/** - * @brief Headline font styles group. - * - * Headline styles are used for app bar important text. - * - * @since 0.1 - * @ingroup ui_core - */ -struct HeadlineFonts { - tokens::HeadlineLargeToken large; ///< 32sp, Regular, 40sp line-height - tokens::HeadlineMediumToken medium; ///< 28sp, Regular, 36sp line-height - tokens::HeadlineSmallToken small; ///< 24sp, Regular, 32sp line-height -}; - -/** - * @brief Title font styles group. - * - * Title styles are used for section headings. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TitleFonts { - tokens::TitleLargeToken large; ///< 22sp, Medium, 28sp line-height - tokens::TitleMediumToken medium; ///< 16sp, Medium, 24sp line-height - tokens::TitleSmallToken small; ///< 14sp, Medium, 20sp line-height -}; - -/** - * @brief Body font styles group. - * - * Body styles are used for main content. - * - * @since 0.1 - * @ingroup ui_core - */ -struct BodyFonts { - tokens::BodyLargeToken large; ///< 16sp, Regular, 24sp line-height - tokens::BodyMediumToken medium; ///< 14sp, Regular, 20sp line-height - tokens::BodySmallToken small; ///< 12sp, Regular, 16sp line-height -}; - -/** - * @brief Label font styles group. - * - * Label styles are used for secondary information. - * - * @since 0.1 - * @ingroup ui_core - */ -struct LabelFonts { - tokens::LabelLargeToken large; ///< 14sp, Medium, 20sp line-height - tokens::LabelMediumToken medium; ///< 12sp, Medium, 16sp line-height - tokens::LabelSmallToken small; ///< 11sp, Medium, 16sp line-height -}; - -// ============================================================================= -// Material Typography - 实现 IFontType 接口 -// ============================================================================= - /** - * @brief Material Design 3 Typography with EmbeddedTokenRegistry. + * @brief Material Design 3 Typography. * * @details Implements the complete Material Design 3 Type Scale system with 15 styles. - * Fonts are stored in an embedded registry for independent typography instances. * * ### Type Scale Specifications * @@ -206,7 +61,7 @@ struct LabelFonts { * * @code * MaterialTypography typography; - * QFont font = typography.queryTargetFont("bodyLarge"); + * QFont font = typography.queryTargetFont("md.typography.bodyLarge"); * @endcode */ class CF_UI_EXPORT MaterialTypography : public IFontType { @@ -249,86 +104,30 @@ class CF_UI_EXPORT MaterialTypography : public IFontType { float getLineHeight(const char* styleName) const; /** - * @brief Access the embedded token registry. - * - * Provides direct access to the internal token registry for - * custom token manipulation. - * - * @return Reference to the EmbeddedTokenRegistry. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - EmbeddedTokenRegistry& registry() { return registry_; } - - /** - * @brief Access the embedded token registry (const overload). - * - * Provides direct read-only access to the internal token registry. - * - * @return Const reference to the EmbeddedTokenRegistry. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - const EmbeddedTokenRegistry& registry() const { return registry_; } - - // Font group accessors - 返回包含 Token 类型的结构体 - /** - * @brief 获取 Display 字体组 - * - * @return DisplayFonts 结构体 - * - * @since 0.1 - */ - [[nodiscard]] DisplayFonts display() const { return {}; } - - /** - * @brief 获取 Headline 字体组 - * - * @return HeadlineFonts 结构体 + * @brief Register a font by name. * - * @since 0.1 - */ - [[nodiscard]] HeadlineFonts headline() const { return {}; } - - /** - * @brief 获取 Title 字体组 - * - * @return TitleFonts 结构体 - * - * @since 0.1 - */ - [[nodiscard]] TitleFonts title() const { return {}; } - - /** - * @brief 获取 Body 字体组 - * - * @return BodyFonts 结构体 + * @param[in] name Font style name (e.g., "md.typography.displayLarge"). + * @param[in] font QFont value to register. * - * @since 0.1 + * @since 0.1 + * @ingroup ui_core */ - [[nodiscard]] BodyFonts body() const { return {}; } + void setFont(const std::string& name, const QFont& font); /** - * @brief 获取 Label 字体组 + * @brief Register a line height by name. * - * @return LabelFonts 结构体 + * @param[in] name Line height token name (e.g., "md.lineHeight.displayLarge"). + * @param[in] lineHeight Line height value in sp. * - * @since 0.1 + * @since 0.1 + * @ingroup ui_core */ - [[nodiscard]] LabelFonts label() const { return {}; } + void setLineHeight(const std::string& name, float lineHeight); private: - EmbeddedTokenRegistry registry_; - mutable std::unordered_map font_cache_; - mutable std::unordered_map line_height_cache_; + std::unordered_map fonts_; + std::unordered_map line_heights_; }; } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_motion.cpp b/ui/core/material/cfmaterial_motion.cpp index a674bfb47..5be27f0d4 100644 --- a/ui/core/material/cfmaterial_motion.cpp +++ b/ui/core/material/cfmaterial_motion.cpp @@ -8,7 +8,7 @@ * @copyright Copyright (c) 2026 * * @details - * Implements MaterialMotionScheme with token registration and query methods. + * Implements MaterialMotionScheme with motion preset registration and query methods. * All motion presets follow Material Design 3 motion specifications. */ #include "cfmaterial_motion.h" @@ -16,75 +16,59 @@ namespace cf::ui::core { MaterialMotionScheme::MaterialMotionScheme() { - // Register all motion tokens - namespace literals = cf::ui::core::token::literals; - auto& r = registry_; - // Durations (Material Design 3 Motion specifications) - r.register_dynamic(literals::MOTION_SHORT_ENTER_DURATION, 200); - r.register_dynamic(literals::MOTION_SHORT_EXIT_DURATION, 150); - r.register_dynamic(literals::MOTION_MEDIUM_ENTER_DURATION, 300); - r.register_dynamic(literals::MOTION_MEDIUM_EXIT_DURATION, 250); - r.register_dynamic(literals::MOTION_LONG_ENTER_DURATION, 450); - r.register_dynamic(literals::MOTION_LONG_EXIT_DURATION, 400); - r.register_dynamic(literals::MOTION_STATE_CHANGE_DURATION, 200); - r.register_dynamic(literals::MOTION_RIPPLE_EXPAND_DURATION, 400); - r.register_dynamic(literals::MOTION_RIPPLE_FADE_DURATION, 150); + durations_["md.motion.shortEnter.duration"] = 200; + durations_["md.motion.shortExit.duration"] = 150; + durations_["md.motion.mediumEnter.duration"] = 300; + durations_["md.motion.mediumExit.duration"] = 250; + durations_["md.motion.longEnter.duration"] = 450; + durations_["md.motion.longExit.duration"] = 400; + durations_["md.motion.stateChange.duration"] = 200; + durations_["md.motion.rippleExpand.duration"] = 400; + durations_["md.motion.rippleFade.duration"] = 150; // Easing types using EType = cf::ui::base::Easing::Type; - r.register_dynamic(literals::MOTION_SHORT_ENTER_EASING, - static_cast(EType::EmphasizedDecelerate)); - r.register_dynamic(literals::MOTION_SHORT_EXIT_EASING, - static_cast(EType::EmphasizedAccelerate)); - r.register_dynamic(literals::MOTION_MEDIUM_ENTER_EASING, - static_cast(EType::EmphasizedDecelerate)); - r.register_dynamic(literals::MOTION_MEDIUM_EXIT_EASING, - static_cast(EType::EmphasizedAccelerate)); - r.register_dynamic(literals::MOTION_LONG_ENTER_EASING, - static_cast(EType::Emphasized)); - r.register_dynamic(literals::MOTION_LONG_EXIT_EASING, static_cast(EType::Emphasized)); - r.register_dynamic(literals::MOTION_STATE_CHANGE_EASING, - static_cast(EType::Standard)); - r.register_dynamic(literals::MOTION_RIPPLE_EXPAND_EASING, - static_cast(EType::Standard)); - r.register_dynamic(literals::MOTION_RIPPLE_FADE_EASING, static_cast(EType::Linear)); + easings_["md.motion.shortEnter.easing"] = static_cast(EType::EmphasizedDecelerate); + easings_["md.motion.shortExit.easing"] = static_cast(EType::EmphasizedAccelerate); + easings_["md.motion.mediumEnter.easing"] = static_cast(EType::EmphasizedDecelerate); + easings_["md.motion.mediumExit.easing"] = static_cast(EType::EmphasizedAccelerate); + easings_["md.motion.longEnter.easing"] = static_cast(EType::Emphasized); + easings_["md.motion.longExit.easing"] = static_cast(EType::Emphasized); + easings_["md.motion.stateChange.easing"] = static_cast(EType::Standard); + easings_["md.motion.rippleExpand.easing"] = static_cast(EType::Standard); + easings_["md.motion.rippleFade.easing"] = static_cast(EType::Linear); } int MaterialMotionScheme::queryDuration(const char* name) { - // Build the full duration token name std::string fullName = std::string("md.motion.") + name + ".duration"; - auto result = registry_.get_dynamic(fullName.c_str()); - return result ? **result : 200; // Default 200ms + auto it = durations_.find(fullName); + return it != durations_.end() ? it->second : 200; } int MaterialMotionScheme::queryEasing(const char* name) { - // Build the full easing token name std::string fullName = std::string("md.motion.") + name + ".easing"; - auto result = registry_.get_dynamic(fullName.c_str()); - return result ? **result : static_cast(cf::ui::base::Easing::Type::Standard); + auto it = easings_.find(fullName); + return it != easings_.end() ? it->second + : static_cast(cf::ui::base::Easing::Type::Standard); } int MaterialMotionScheme::queryDelay(const char* name) { - // Motion specs default to 0 delay - (void)name; // Suppress unused parameter warning + (void)name; return 0; } MotionSpec MaterialMotionScheme::getMotionSpec(const char* name) { - // Check cache first auto it = spec_cache_.find(name); if (it != spec_cache_.end()) { return it->second; } - // Build new spec MotionSpec spec; spec.durationMs = queryDuration(name); spec.easing = static_cast(queryEasing(name)); spec.delayMs = queryDelay(name); - // Cache for future queries spec_cache_[name] = spec; return spec; } diff --git a/ui/core/material/cfmaterial_motion.h b/ui/core/material/cfmaterial_motion.h index a8ecd514f..42420c983 100644 --- a/ui/core/material/cfmaterial_motion.h +++ b/ui/core/material/cfmaterial_motion.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_motion.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Motion Scheme with EmbeddedTokenRegistry + * @brief Material Design 3 Motion Scheme * @version 0.1 * @date 2026-02-26 * @@ -9,8 +9,7 @@ * * @details * Implements the complete Material Design 3 motion system with duration, - * easing, and delay specifications. Motion specs are stored in an embedded - * registry for independent scheme instances. + * easing, and delay specifications. */ #pragma once #include @@ -18,45 +17,11 @@ #include #include "../motion_spec.h" -#include "../token.hpp" -#include "../token/motion/cfmaterial_motion_token_literals.h" #include "base/easing.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "export.h" namespace cf::ui::core { -// ============================================================================= -// Motion Token Type Aliases - Material Motion System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Duration tokens -using ShortEnterDurationToken = StaticToken; -using ShortExitDurationToken = StaticToken; -using MediumEnterDurationToken = StaticToken; -using MediumExitDurationToken = StaticToken; -using LongEnterDurationToken = StaticToken; -using LongExitDurationToken = StaticToken; -using StateChangeDurationToken = StaticToken; -using RippleExpandDurationToken = - StaticToken; -using RippleFadeDurationToken = StaticToken; - -// Easing tokens (stored as int enum values) -using ShortEnterEasingToken = StaticToken; -using ShortExitEasingToken = StaticToken; -using MediumEnterEasingToken = StaticToken; -using MediumExitEasingToken = StaticToken; -using LongEnterEasingToken = StaticToken; -using LongExitEasingToken = StaticToken; -using StateChangeEasingToken = StaticToken; -using RippleExpandEasingToken = StaticToken; -using RippleFadeEasingToken = StaticToken; - -} // namespace tokens - // ============================================================================= // Motion Data Structure // ============================================================================= @@ -65,8 +30,7 @@ using RippleFadeEasingToken = StaticToken durations_; + std::unordered_map easings_; mutable std::unordered_map spec_cache_; }; diff --git a/ui/core/material/cfmaterial_radius_scale.cpp b/ui/core/material/cfmaterial_radius_scale.cpp index 8b0098f9f..a76446d38 100644 --- a/ui/core/material/cfmaterial_radius_scale.cpp +++ b/ui/core/material/cfmaterial_radius_scale.cpp @@ -14,39 +14,22 @@ namespace cf::ui::core { MaterialRadiusScale::MaterialRadiusScale() { - registerDefaultCorners(); -} - -void MaterialRadiusScale::registerDefaultCorners() { - namespace literals = cf::ui::core::token::literals; - auto& r = registry_; - - // 注册默认值(Material Design 3 规范) - r.register_dynamic(literals::CORNER_NONE, 0.0f); - r.register_dynamic(literals::CORNER_EXTRA_SMALL, 4.0f); - r.register_dynamic(literals::CORNER_SMALL, 8.0f); - r.register_dynamic(literals::CORNER_MEDIUM, 12.0f); - r.register_dynamic(literals::CORNER_LARGE, 16.0f); - r.register_dynamic(literals::CORNER_EXTRA_LARGE, 28.0f); - r.register_dynamic(literals::CORNER_EXTRA_EXTRA_LARGE, 32.0f); + radii_["md.shape.cornerNone"] = 0.0f; + radii_["md.shape.cornerExtraSmall"] = 4.0f; + radii_["md.shape.cornerSmall"] = 8.0f; + radii_["md.shape.cornerMedium"] = 12.0f; + radii_["md.shape.cornerLarge"] = 16.0f; + radii_["md.shape.cornerExtraLarge"] = 28.0f; + radii_["md.shape.cornerExtraExtraLarge"] = 32.0f; } float MaterialRadiusScale::queryRadiusScale(const char* name) { - // 先查缓存 - auto it = radius_cache_.find(name); - if (it != radius_cache_.end()) { - return it->second; - } - - // 从注册表获取 - auto result = registry_.get_dynamic(name); - if (result && *result) { - auto [iter, inserted] = radius_cache_.emplace(name, **result); - return iter->second; - } + auto it = radii_.find(name); + return it != radii_.end() ? it->second : 0.0f; +} - // 默认回退值 - return 0.0f; +void MaterialRadiusScale::setRadius(const std::string& name, float radius) { + radii_[name] = radius; } } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_radius_scale.h b/ui/core/material/cfmaterial_radius_scale.h index 887401132..1e9e2efdd 100644 --- a/ui/core/material/cfmaterial_radius_scale.h +++ b/ui/core/material/cfmaterial_radius_scale.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_radius_scale.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Radius Scale with EmbeddedTokenRegistry + * @brief Material Design 3 Radius Scale * @version 0.1 * @date 2026-02-26 * @@ -9,7 +9,6 @@ * * @details * Implements the complete Material Design 3 corner radius system with 7 scales. - * Radii are stored in an embedded registry for independent radius scale instances. */ #pragma once @@ -18,50 +17,26 @@ #include #include "../export.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "radius_scale.h" -#include "token.hpp" -#include "token/radius_scale/cfmaterial_radius_scale_literals.h" namespace cf::ui::core { -// ============================================================================= -// Radius Scale Token Type Aliases - Material Radius Scale System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -using CornerNoneToken = StaticToken; -using CornerExtraSmallToken = StaticToken; -using CornerSmallToken = StaticToken; -using CornerMediumToken = StaticToken; -using CornerLargeToken = StaticToken; -using CornerExtraLargeToken = StaticToken; -using CornerExtraExtraLargeToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Material Radius Scale - 实现 IRadiusScale 接口 -// ============================================================================= - /** - * @brief Material Design 3 Radius Scale with EmbeddedTokenRegistry. + * @brief Material Design 3 Radius Scale. * * @details Implements the complete Material Design 3 Corner Radius system with 7 scales. - * Radii are stored in an embedded registry for independent radius scale instances. * * ### Radius Scale Specifications * * | Token | Name | Value (dp) | Usage | * |-------|------|------------|-------| - * | CORNER_NONE | cornerNone | 0dp | No corner radius | - * | CORNER_EXTRA_SMALL | cornerExtraSmall | 4dp | Chips, small cards | - * | CORNER_SMALL | cornerSmall | 8dp | Text fields, checkboxes | - * | CORNER_MEDIUM | cornerMedium | 12dp | Cards | - * | CORNER_LARGE | cornerLarge | 16dp | Alert dialogs | - * | CORNER_EXTRA_LARGE | cornerExtraLarge | 28dp | FAB, modals | - * | CORNER_EXTRA_EXTRA_LARGE | cornerExtraExtraLarge | 32dp | Drawers | + * | cornerNone | cornerNone | 0dp | No corner radius | + * | cornerExtraSmall | cornerExtraSmall | 4dp | Chips, small cards | + * | cornerSmall | cornerSmall | 8dp | Text fields, checkboxes | + * | cornerMedium | cornerMedium | 12dp | Cards | + * | cornerLarge | cornerLarge | 16dp | Alert dialogs | + * | cornerExtraLarge | cornerExtraLarge | 28dp | FAB, modals | + * | cornerExtraExtraLarge | cornerExtraExtraLarge | 32dp | Drawers | * * @note None * @warning None @@ -71,7 +46,7 @@ using CornerExtraExtraLargeToken = StaticToken radius_cache_; + std::unordered_map radii_; }; } // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.cpp b/ui/core/material/cfmaterial_scheme.cpp index 44454d6e5..fd2a78b1e 100644 --- a/ui/core/material/cfmaterial_scheme.cpp +++ b/ui/core/material/cfmaterial_scheme.cpp @@ -14,38 +14,32 @@ namespace cf::ui::core { MaterialColorScheme::MaterialColorScheme() { - color_cache_.reserve(32); + colors_.reserve(32); } QColor& MaterialColorScheme::queryExpectedColor(const char* name) { - auto it = color_cache_.find(name); - if (it != color_cache_.end()) { + auto it = colors_.find(name); + if (it != colors_.end()) { return it->second; } - - auto result = registry_.get_dynamic(name); - if (result && *result) { - auto [iter, inserted] = color_cache_.emplace(name, (*result)->native_color()); - return iter->second; - } - - // Fallback color static QColor fallback(Qt::black); return fallback; } QColor MaterialColorScheme::queryColor(const char* name) const { - auto it = color_cache_.find(name); - if (it != color_cache_.end()) { + auto it = colors_.find(name); + if (it != colors_.end()) { return it->second; } + return QColor(Qt::black); +} - auto result = registry_.get_dynamic_const(name); - if (result && *result) { - return (*result)->native_color(); - } +void MaterialColorScheme::setColor(const std::string& name, const QColor& color) { + colors_[name] = color; +} - return QColor(Qt::black); +bool MaterialColorScheme::hasColor(const std::string& name) const { + return colors_.find(name) != colors_.end(); } -} // namespace cf::ui::core \ No newline at end of file +} // namespace cf::ui::core diff --git a/ui/core/material/cfmaterial_scheme.h b/ui/core/material/cfmaterial_scheme.h index 06aebeb0e..3f6f5d958 100644 --- a/ui/core/material/cfmaterial_scheme.h +++ b/ui/core/material/cfmaterial_scheme.h @@ -1,7 +1,7 @@ /** * @file cfmaterial_scheme.h * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) - * @brief Material Design 3 Color Scheme with EmbeddedTokenRegistry + * @brief Material Design 3 Color Scheme * @version 0.1 * @date 2026-02-25 * @@ -9,185 +9,20 @@ * */ #pragma once -#include #include #include #include #include "../../export.h" -#include "base/color.h" -#include "base/hash/constexpr_fnv1a.hpp" #include "color_scheme.h" -#include "token.hpp" -#include "token/material_scheme/cfmaterial_token_literals.h" namespace cf::ui::core { -using CFColor = cf::ui::base::CFColor; - -// ============================================================================= -// Color Token Type Aliases - Material Color System -// ============================================================================= -namespace tokens { -using namespace cf::ui::core::token::literals; - -// Primary color tokens -using PrimaryToken = StaticToken; -using OnPrimaryToken = StaticToken; -using PrimaryContainerToken = StaticToken; -using OnPrimaryContainerToken = StaticToken; - -// Secondary color tokens -using SecondaryToken = StaticToken; -using OnSecondaryToken = StaticToken; -using SecondaryContainerToken = StaticToken; -using OnSecondaryContainerToken = StaticToken; - -// Tertiary color tokens -using TertiaryToken = StaticToken; -using OnTertiaryToken = StaticToken; -using TertiaryContainerToken = StaticToken; -using OnTertiaryContainerToken = StaticToken; - -// Error color tokens -using ErrorToken = StaticToken; -using OnErrorToken = StaticToken; -using ErrorContainerToken = StaticToken; -using OnErrorContainerToken = StaticToken; - -// Surface color tokens -using BackgroundToken = StaticToken; -using OnBackgroundToken = StaticToken; -using SurfaceToken = StaticToken; -using OnSurfaceToken = StaticToken; -using SurfaceVariantToken = StaticToken; -using OnSurfaceVariantToken = StaticToken; -using OutlineToken = StaticToken; -using OutlineVariantToken = StaticToken; - -// Utility color tokens -using ShadowToken = StaticToken; -using ScrimToken = StaticToken; -using InverseSurfaceToken = StaticToken; -using InverseOnSurfaceToken = StaticToken; -using InversePrimaryToken = StaticToken; - -} // namespace tokens - -// ============================================================================= -// Color Group Structs - Material Color System -// ============================================================================= - /** - * @brief Primary color group. - * - * Primary colors are used for key components throughout the UI, - * such as prominent buttons, active states, and tonal surfaces. - * - * @since 0.1 - * @ingroup ui_core - */ -struct PrimaryColors { - tokens::PrimaryToken primary; - tokens::OnPrimaryToken onPrimary; - tokens::PrimaryContainerToken primaryContainer; - tokens::OnPrimaryContainerToken onPrimaryContainer; -}; - -/** - * @brief Secondary color group. - * - * Secondary colors provide alternative ways to distinguish components. - * They are used for less prominent components and for accents. - * - * @since 0.1 - * @ingroup ui_core - */ -struct SecondaryColors { - tokens::SecondaryToken secondary; - tokens::OnSecondaryToken onSecondary; - tokens::SecondaryContainerToken secondaryContainer; - tokens::OnSecondaryContainerToken onSecondaryContainer; -}; - -/** - * @brief Tertiary color group. - * - * Tertiary colors are used to derive contrasting accents that can be used - * for balance and to express unique branding. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TertiaryColors { - tokens::TertiaryToken tertiary; - tokens::OnTertiaryToken onTertiary; - tokens::TertiaryContainerToken tertiaryContainer; - tokens::OnTertiaryContainerToken onTertiaryContainer; -}; - -/** - * @brief Error color group. - * - * Error colors are used for error states, destructive actions, - * and validation feedback. - * - * @since 0.1 - * @ingroup ui_core - */ -struct ErrorColors { - tokens::ErrorToken error; - tokens::OnErrorToken onError; - tokens::ErrorContainerToken errorContainer; - tokens::OnErrorContainerToken onErrorContainer; -}; - -/** - * @brief Background and Surface color group. - * - * These colors define the base surfaces of the UI, including - * backgrounds, surfaces, and their variants. - * - * @since 0.1 - * @ingroup ui_core - */ -struct SurfaceColors { - tokens::BackgroundToken background; - tokens::OnBackgroundToken onBackground; - tokens::SurfaceToken surface; - tokens::OnSurfaceToken onSurface; - tokens::SurfaceVariantToken surfaceVariant; - tokens::OnSurfaceVariantToken onSurfaceVariant; - tokens::OutlineToken outline; - tokens::OutlineVariantToken outlineVariant; -}; - -/** - * @brief Utility color group. - * - * Utility colors support elevated surfaces and inverse states - * for special UI scenarios. - * - * @since 0.1 - * @ingroup ui_core - */ -struct UtilityColors { - tokens::ShadowToken shadow; - tokens::ScrimToken scrim; - tokens::InverseSurfaceToken inverseSurface; - tokens::InverseOnSurfaceToken inverseOnSurface; - tokens::InversePrimaryToken inversePrimary; -}; - -// ============================================================================= -// Material Color Scheme -// ============================================================================= - -/** - * @brief Material Design 3 Color Scheme with EmbeddedTokenRegistry. + * @brief Material Design 3 Color Scheme. * * @details Implements the complete Material Design 3 color system with 26 color tokens. - * Colors are stored in an embedded registry for independent scheme instances. + * Colors are stored in a flat map keyed by string token names. * * Factory functions are available in the cf::ui::core::material namespace. * @@ -227,7 +62,7 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { * * @throws None. * - * @note Returns a cached reference that is valid for the lifetime + * @note Returns a reference that is valid for the lifetime * of the color scheme. * * @warning None. @@ -246,7 +81,7 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { * * @throws None. * - * @note Returns a cached copy or default color if not found. + * @note Returns a default black color if not found. * * @warning None. * @@ -256,48 +91,30 @@ class CF_UI_EXPORT MaterialColorScheme : public ICFColorScheme { QColor queryColor(const char* name) const; /** - * @brief Access the embedded token registry. - * - * @return Reference to the EmbeddedTokenRegistry. - * - * @throws None. + * @brief Register a color by name. * - * @note Provides direct access to internal token storage. - * - * @warning Modifying tokens directly may affect color scheme behavior. + * @param[in] name Color token name (e.g., "md.primary"). + * @param[in] color Color value to register. * * @since 0.1 * @ingroup ui_core */ - EmbeddedTokenRegistry& registry() { return registry_; } + void setColor(const std::string& name, const QColor& color); /** - * @brief Access the embedded token registry (const overload). - * - * @return Const reference to the EmbeddedTokenRegistry. - * - * @throws None. + * @brief Check if a color token exists. * - * @note Read-only access to internal token storage. + * @param[in] name Color token name. * - * @warning None. + * @return true if the token exists. * * @since 0.1 * @ingroup ui_core */ - const EmbeddedTokenRegistry& registry() const { return registry_; } - - // Color group accessors - return structs with token types - [[nodiscard]] PrimaryColors primary() const { return {}; } - [[nodiscard]] SecondaryColors secondary() const { return {}; } - [[nodiscard]] TertiaryColors tertiary() const { return {}; } - [[nodiscard]] ErrorColors error() const { return {}; } - [[nodiscard]] SurfaceColors surface() const { return {}; } - [[nodiscard]] UtilityColors utility() const { return {}; } + bool hasColor(const std::string& name) const; private: - EmbeddedTokenRegistry registry_; - mutable std::unordered_map color_cache_; + std::unordered_map colors_; }; } // namespace cf::ui::core diff --git a/ui/core/material/material_factory.cpp b/ui/core/material/material_factory.cpp index b6e50cace..5b68a156c 100644 --- a/ui/core/material/material_factory.cpp +++ b/ui/core/material/material_factory.cpp @@ -20,11 +20,6 @@ #include "cfmaterial_fonttype.h" #include "cfmaterial_motion.h" #include "cfmaterial_radius_scale.h" -#include "token/material_scheme/cfmaterial_token_literals.h" - -// Type aliases to avoid namespace lookup issues -using CFColor = ::cf::ui::base::CFColor; - namespace cf::ui::core::material { namespace detail { @@ -123,55 +118,46 @@ struct DarkColors { // ============================================================================= template inline void registerAllColors(MaterialColorScheme& scheme) { - auto& r = scheme.registry(); - namespace literals = ::cf::ui::core::token::literals; - using CFColor = ::cf::ui::base::CFColor; - // Primary colors - r.register_dynamic(literals::PRIMARY, CFColor(ColorDefs::primary)); - r.register_dynamic(literals::ON_PRIMARY, CFColor(ColorDefs::onPrimary)); - r.register_dynamic(literals::PRIMARY_CONTAINER, CFColor(ColorDefs::primaryContainer)); - r.register_dynamic(literals::ON_PRIMARY_CONTAINER, - CFColor(ColorDefs::onPrimaryContainer)); + scheme.setColor("md.primary", QColor(ColorDefs::primary)); + scheme.setColor("md.onPrimary", QColor(ColorDefs::onPrimary)); + scheme.setColor("md.primaryContainer", QColor(ColorDefs::primaryContainer)); + scheme.setColor("md.onPrimaryContainer", QColor(ColorDefs::onPrimaryContainer)); // Secondary colors - r.register_dynamic(literals::SECONDARY, CFColor(ColorDefs::secondary)); - r.register_dynamic(literals::ON_SECONDARY, CFColor(ColorDefs::onSecondary)); - r.register_dynamic(literals::SECONDARY_CONTAINER, - CFColor(ColorDefs::secondaryContainer)); - r.register_dynamic(literals::ON_SECONDARY_CONTAINER, - CFColor(ColorDefs::onSecondaryContainer)); + scheme.setColor("md.secondary", QColor(ColorDefs::secondary)); + scheme.setColor("md.onSecondary", QColor(ColorDefs::onSecondary)); + scheme.setColor("md.secondaryContainer", QColor(ColorDefs::secondaryContainer)); + scheme.setColor("md.onSecondaryContainer", QColor(ColorDefs::onSecondaryContainer)); // Tertiary colors - r.register_dynamic(literals::TERTIARY, CFColor(ColorDefs::tertiary)); - r.register_dynamic(literals::ON_TERTIARY, CFColor(ColorDefs::onTertiary)); - r.register_dynamic(literals::TERTIARY_CONTAINER, - CFColor(ColorDefs::tertiaryContainer)); - r.register_dynamic(literals::ON_TERTIARY_CONTAINER, - CFColor(ColorDefs::onTertiaryContainer)); + scheme.setColor("md.tertiary", QColor(ColorDefs::tertiary)); + scheme.setColor("md.onTertiary", QColor(ColorDefs::onTertiary)); + scheme.setColor("md.tertiaryContainer", QColor(ColorDefs::tertiaryContainer)); + scheme.setColor("md.onTertiaryContainer", QColor(ColorDefs::onTertiaryContainer)); // Error colors - r.register_dynamic(literals::ERROR, CFColor(ColorDefs::error)); - r.register_dynamic(literals::ON_ERROR, CFColor(ColorDefs::onError)); - r.register_dynamic(literals::ERROR_CONTAINER, CFColor(ColorDefs::errorContainer)); - r.register_dynamic(literals::ON_ERROR_CONTAINER, CFColor(ColorDefs::onErrorContainer)); + scheme.setColor("md.error", QColor(ColorDefs::error)); + scheme.setColor("md.onError", QColor(ColorDefs::onError)); + scheme.setColor("md.errorContainer", QColor(ColorDefs::errorContainer)); + scheme.setColor("md.onErrorContainer", QColor(ColorDefs::onErrorContainer)); // Surface colors - r.register_dynamic(literals::BACKGROUND, CFColor(ColorDefs::background)); - r.register_dynamic(literals::ON_BACKGROUND, CFColor(ColorDefs::onBackground)); - r.register_dynamic(literals::SURFACE, CFColor(ColorDefs::surface)); - r.register_dynamic(literals::ON_SURFACE, CFColor(ColorDefs::onSurface)); - r.register_dynamic(literals::SURFACE_VARIANT, CFColor(ColorDefs::surfaceVariant)); - r.register_dynamic(literals::ON_SURFACE_VARIANT, CFColor(ColorDefs::onSurfaceVariant)); - r.register_dynamic(literals::OUTLINE, CFColor(ColorDefs::outline)); - r.register_dynamic(literals::OUTLINE_VARIANT, CFColor(ColorDefs::outlineVariant)); + scheme.setColor("md.background", QColor(ColorDefs::background)); + scheme.setColor("md.onBackground", QColor(ColorDefs::onBackground)); + scheme.setColor("md.surface", QColor(ColorDefs::surface)); + scheme.setColor("md.onSurface", QColor(ColorDefs::onSurface)); + scheme.setColor("md.surfaceVariant", QColor(ColorDefs::surfaceVariant)); + scheme.setColor("md.onSurfaceVariant", QColor(ColorDefs::onSurfaceVariant)); + scheme.setColor("md.outline", QColor(ColorDefs::outline)); + scheme.setColor("md.outlineVariant", QColor(ColorDefs::outlineVariant)); // Utility colors - r.register_dynamic(literals::SHADOW, CFColor(ColorDefs::shadow)); - r.register_dynamic(literals::SCRIM, CFColor(ColorDefs::scrim)); - r.register_dynamic(literals::INVERSE_SURFACE, CFColor(ColorDefs::inverseSurface)); - r.register_dynamic(literals::INVERSE_ON_SURFACE, CFColor(ColorDefs::inverseOnSurface)); - r.register_dynamic(literals::INVERSE_PRIMARY, CFColor(ColorDefs::inversePrimary)); + scheme.setColor("md.shadow", QColor(ColorDefs::shadow)); + scheme.setColor("md.scrim", QColor(ColorDefs::scrim)); + scheme.setColor("md.inverseSurface", QColor(ColorDefs::inverseSurface)); + scheme.setColor("md.inverseOnSurface", QColor(ColorDefs::inverseOnSurface)); + scheme.setColor("md.inversePrimary", QColor(ColorDefs::inversePrimary)); } } // namespace detail @@ -220,7 +206,6 @@ Result fromJson(const QByteArray& json, bool isDark) { } MaterialColorScheme scheme; - auto& r = scheme.registry(); // Map of MD3 color names to registry keys const std::unordered_map colorMapping = { @@ -264,144 +249,135 @@ Result fromJson(const QByteArray& json, bool isDark) { MaterialSchemeError::Kind::InvalidColorFormat, "Invalid color format for " + key + ": " + colorStr.toStdString()}); } - r.register_dynamic(registryKey, CFColor(colorStr)); + scheme.setColor(registryKey, QColor(colorStr)); } } return scheme; } -MaterialColorScheme fromKeyColor(CFColor keyColor, bool isDark) { +MaterialColorScheme fromKeyColor(cf::ui::base::CFColor keyColor, bool isDark) { using ::cf::ui::base::tonalPalette; MaterialColorScheme scheme; - auto& r = scheme.registry(); // Generate tonal palette from key color - QList tonal = tonalPalette(keyColor); + QList tonal = tonalPalette(keyColor); // Helper to get color from tonal palette by index - auto getTone = [&tonal](int index) -> CFColor { - // tonalPalette returns 13 colors: 0, 10, 20, ..., 90, 95, 99, 100 - // Map index 0-12 to these tones + auto getTone = [&tonal](int index) -> cf::ui::base::CFColor { if (index >= 0 && index < tonal.size()) { return tonal[index]; } - return CFColor("#808080"); // Fallback gray + return cf::ui::base::CFColor("#808080"); // Fallback gray }; if (!isDark) { // Light scheme generation - r.register_dynamic("md.primary", getTone(4)); // Tone 40 - r.register_dynamic("md.onPrimary", CFColor("#FFFFFF")); - r.register_dynamic("md.primaryContainer", getTone(9)); // Tone 90 - r.register_dynamic("md.onPrimaryContainer", getTone(0)); // Tone 0 + scheme.setColor("md.primary", getTone(4).native_color()); + scheme.setColor("md.onPrimary", QColor("#FFFFFF")); + scheme.setColor("md.primaryContainer", getTone(9).native_color()); + scheme.setColor("md.onPrimaryContainer", getTone(0).native_color()); // Secondary: Use complementary hue - CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), keyColor.chroma() * 0.8f, - keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - r.register_dynamic("md.secondary", secondaryPalette[5]); - r.register_dynamic("md.onSecondary", CFColor("#FFFFFF")); - r.register_dynamic("md.secondaryContainer", secondaryPalette[9]); - r.register_dynamic("md.onSecondaryContainer", secondaryPalette[0]); + cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), + keyColor.chroma() * 0.8f, keyColor.tone()); + QList secondaryPalette = tonalPalette(secondaryKey); + scheme.setColor("md.secondary", secondaryPalette[5].native_color()); + scheme.setColor("md.onSecondary", QColor("#FFFFFF")); + scheme.setColor("md.secondaryContainer", secondaryPalette[9].native_color()); + scheme.setColor("md.onSecondaryContainer", secondaryPalette[0].native_color()); // Tertiary: Use complementary hue - CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), keyColor.chroma() * 0.6f, - keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - r.register_dynamic("md.tertiary", tertiaryPalette[5]); - r.register_dynamic("md.onTertiary", CFColor("#FFFFFF")); - r.register_dynamic("md.tertiaryContainer", tertiaryPalette[9]); - r.register_dynamic("md.onTertiaryContainer", tertiaryPalette[0]); + cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), + keyColor.chroma() * 0.6f, keyColor.tone()); + QList tertiaryPalette = tonalPalette(tertiaryKey); + scheme.setColor("md.tertiary", tertiaryPalette[5].native_color()); + scheme.setColor("md.onTertiary", QColor("#FFFFFF")); + scheme.setColor("md.tertiaryContainer", tertiaryPalette[9].native_color()); + scheme.setColor("md.onTertiaryContainer", tertiaryPalette[0].native_color()); // Error colors (fixed red-based) - r.register_dynamic("md.error", CFColor("#B3261E")); - r.register_dynamic("md.onError", CFColor("#FFFFFF")); - r.register_dynamic("md.errorContainer", CFColor("#F9DEDC")); - r.register_dynamic("md.onErrorContainer", CFColor("#410E0B")); + scheme.setColor("md.error", QColor("#B3261E")); + scheme.setColor("md.onError", QColor("#FFFFFF")); + scheme.setColor("md.errorContainer", QColor("#F9DEDC")); + scheme.setColor("md.onErrorContainer", QColor("#410E0B")); // Surface colors - r.register_dynamic("md.background", CFColor("#FFFBFE")); - r.register_dynamic("md.onBackground", CFColor("#1C1B1F")); - r.register_dynamic("md.surface", CFColor("#FFFBFE")); - r.register_dynamic("md.onSurface", CFColor("#1C1B1F")); - r.register_dynamic("md.surfaceVariant", getTone(11)); - r.register_dynamic("md.onSurfaceVariant", getTone(2)); - r.register_dynamic("md.outline", getTone(7)); - r.register_dynamic("md.outlineVariant", getTone(10)); + scheme.setColor("md.background", QColor("#FFFBFE")); + scheme.setColor("md.onBackground", QColor("#1C1B1F")); + scheme.setColor("md.surface", QColor("#FFFBFE")); + scheme.setColor("md.onSurface", QColor("#1C1B1F")); + scheme.setColor("md.surfaceVariant", getTone(11).native_color()); + scheme.setColor("md.onSurfaceVariant", getTone(2).native_color()); + scheme.setColor("md.outline", getTone(7).native_color()); + scheme.setColor("md.outlineVariant", getTone(10).native_color()); // Utility colors - r.register_dynamic("md.shadow", CFColor("#000000")); - r.register_dynamic("md.scrim", CFColor("#000000")); - r.register_dynamic("md.inverseSurface", CFColor("#313033")); - r.register_dynamic("md.inverseOnSurface", CFColor("#F4EFF4")); - r.register_dynamic("md.inversePrimary", getTone(8)); + scheme.setColor("md.shadow", QColor("#000000")); + scheme.setColor("md.scrim", QColor("#000000")); + scheme.setColor("md.inverseSurface", QColor("#313033")); + scheme.setColor("md.inverseOnSurface", QColor("#F4EFF4")); + scheme.setColor("md.inversePrimary", getTone(8).native_color()); } else { // Dark scheme generation - r.register_dynamic("md.primary", getTone(8)); // Tone 80 - r.register_dynamic("md.onPrimary", getTone(0)); // Tone 0 - r.register_dynamic("md.primaryContainer", getTone(3)); // Tone 30 - r.register_dynamic("md.onPrimaryContainer", getTone(11)); // Tone 95 + scheme.setColor("md.primary", getTone(8).native_color()); + scheme.setColor("md.onPrimary", getTone(0).native_color()); + scheme.setColor("md.primaryContainer", getTone(3).native_color()); + scheme.setColor("md.onPrimaryContainer", getTone(11).native_color()); // Secondary - CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), keyColor.chroma() * 0.8f, - keyColor.tone()); - QList secondaryPalette = tonalPalette(secondaryKey); - r.register_dynamic("md.secondary", secondaryPalette[8]); - r.register_dynamic("md.onSecondary", secondaryPalette[0]); - r.register_dynamic("md.secondaryContainer", secondaryPalette[3]); - r.register_dynamic("md.onSecondaryContainer", secondaryPalette[11]); + cf::ui::base::CFColor secondaryKey(std::fmod(keyColor.hue() + 60.0f, 360.0f), + keyColor.chroma() * 0.8f, keyColor.tone()); + QList secondaryPalette = tonalPalette(secondaryKey); + scheme.setColor("md.secondary", secondaryPalette[8].native_color()); + scheme.setColor("md.onSecondary", secondaryPalette[0].native_color()); + scheme.setColor("md.secondaryContainer", secondaryPalette[3].native_color()); + scheme.setColor("md.onSecondaryContainer", secondaryPalette[11].native_color()); // Tertiary - CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), keyColor.chroma() * 0.6f, - keyColor.tone()); - QList tertiaryPalette = tonalPalette(tertiaryKey); - r.register_dynamic("md.tertiary", tertiaryPalette[8]); - r.register_dynamic("md.onTertiary", tertiaryPalette[0]); - r.register_dynamic("md.tertiaryContainer", tertiaryPalette[3]); - r.register_dynamic("md.onTertiaryContainer", tertiaryPalette[11]); + cf::ui::base::CFColor tertiaryKey(std::fmod(keyColor.hue() + 120.0f, 360.0f), + keyColor.chroma() * 0.6f, keyColor.tone()); + QList tertiaryPalette = tonalPalette(tertiaryKey); + scheme.setColor("md.tertiary", tertiaryPalette[8].native_color()); + scheme.setColor("md.onTertiary", tertiaryPalette[0].native_color()); + scheme.setColor("md.tertiaryContainer", tertiaryPalette[3].native_color()); + scheme.setColor("md.onTertiaryContainer", tertiaryPalette[11].native_color()); // Error colors (fixed red-based for dark) - r.register_dynamic("md.error", CFColor("#F2B8B5")); - r.register_dynamic("md.onError", CFColor("#601410")); - r.register_dynamic("md.errorContainer", CFColor("#8C1D18")); - r.register_dynamic("md.onErrorContainer", CFColor("#F9DEDC")); + scheme.setColor("md.error", QColor("#F2B8B5")); + scheme.setColor("md.onError", QColor("#601410")); + scheme.setColor("md.errorContainer", QColor("#8C1D18")); + scheme.setColor("md.onErrorContainer", QColor("#F9DEDC")); // Surface colors - r.register_dynamic("md.background", CFColor("#1C1B1F")); - r.register_dynamic("md.onBackground", CFColor("#E6E1E5")); - r.register_dynamic("md.surface", CFColor("#1C1B1F")); - r.register_dynamic("md.onSurface", CFColor("#E6E1E5")); - r.register_dynamic("md.surfaceVariant", getTone(2)); - r.register_dynamic("md.onSurfaceVariant", getTone(10)); - r.register_dynamic("md.outline", getTone(5)); - r.register_dynamic("md.outlineVariant", getTone(2)); + scheme.setColor("md.background", QColor("#1C1B1F")); + scheme.setColor("md.onBackground", QColor("#E6E1E5")); + scheme.setColor("md.surface", QColor("#1C1B1F")); + scheme.setColor("md.onSurface", QColor("#E6E1E5")); + scheme.setColor("md.surfaceVariant", getTone(2).native_color()); + scheme.setColor("md.onSurfaceVariant", getTone(10).native_color()); + scheme.setColor("md.outline", getTone(5).native_color()); + scheme.setColor("md.outlineVariant", getTone(2).native_color()); // Utility colors - r.register_dynamic("md.shadow", CFColor("#000000")); - r.register_dynamic("md.scrim", CFColor("#000000")); - r.register_dynamic("md.inverseSurface", CFColor("#E6E1E5")); - r.register_dynamic("md.inverseOnSurface", CFColor("#313033")); - r.register_dynamic("md.inversePrimary", getTone(4)); + scheme.setColor("md.shadow", QColor("#000000")); + scheme.setColor("md.scrim", QColor("#000000")); + scheme.setColor("md.inverseSurface", QColor("#E6E1E5")); + scheme.setColor("md.inverseOnSurface", QColor("#313033")); + scheme.setColor("md.inversePrimary", getTone(4).native_color()); } return scheme; } QByteArray toJson(const MaterialColorScheme& scheme) { - QJsonObject root; QJsonObject lightScheme; - auto& r = scheme.registry(); - - // Helper to get color string from registry - auto getColorString = [&r](const char* key) -> QString { - auto result = r.get_dynamic_const<::cf::ui::base::CFColor>(key); - if (result && *result) { - return (*result)->native_color().name(); - } - return "#000000"; + // Helper to get color string from scheme + auto getColorString = [&scheme](const char* key) -> QString { + QColor color = scheme.queryColor(key); + return color.isValid() ? color.name() : "#000000"; }; // Build scheme object @@ -438,6 +414,7 @@ QByteArray toJson(const MaterialColorScheme& scheme) { QJsonObject schemes; schemes["light"] = lightScheme; + QJsonObject root; root["schemes"] = schemes; QJsonDocument doc(root); diff --git a/ui/core/material/material_factory.hpp b/ui/core/material/material_factory.hpp index d9805d341..e48e3c653 100644 --- a/ui/core/material/material_factory.hpp +++ b/ui/core/material/material_factory.hpp @@ -14,6 +14,7 @@ #include #include "../../export.h" +#include "base/color.h" #include "base/expected/expected.hpp" #include "cfmaterial_fonttype.h" #include "cfmaterial_motion.h" diff --git a/ui/core/token.hpp b/ui/core/token.hpp deleted file mode 100644 index fc8f23399..000000000 --- a/ui/core/token.hpp +++ /dev/null @@ -1,1018 +0,0 @@ -/** - * @file token.hpp - * @brief High-performance type-safe token system with runtime registration. - * - * Provides a dual-mode token system: - * 1. StaticToken: Compile-time type-safe tokens with zero overhead - * 2. DynamicToken: Runtime type-erased tokens using std::any - * 3. TokenRegistry: Read-safe registry using shared_mutex for read-heavy workloads - * - * @author Charliechen114514 - * @date 2026-02-25 - * @version 0.1 - * @since 0.1 - * @ingroup ui_core - */ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "base/expected/expected.hpp" -#include "base/hash/constexpr_fnv1a.hpp" - -namespace cf::ui::core { - -// ============================================================================= -// Forward Declarations -// ============================================================================= -class TokenRegistry; -/// @brief Compile-time type-safe token with zero runtime overhead. -template class StaticToken; - -// ============================================================================= -// Error Types -// ============================================================================= -/** - * @brief Error type for Token operations. - * - * @since 0.1 - * @ingroup ui_core - */ -struct TokenError { - /** - * @brief Error kind enumeration. - * - * @since 0.1 - */ - enum class Kind { - NotFound, ///< Token not found in registry - TypeMismatch, ///< Type mismatch during get() - AlreadyRegistered, ///< Token already registered - Empty ///< Token has no value - } kind = Kind::NotFound; - - /** - * @brief Human-readable error message. - * - * @since 0.1 - */ - std::string message; - - /** - * @brief Default constructor. - * - * @since 0.1 - */ - TokenError() = default; - - /** - * @brief Construct from kind and message. - * - * @param k Error kind. - * @param msg Error message. - * - * @since 0.1 - */ - TokenError(Kind k, std::string msg) : kind(k), message(std::move(msg)) {} - - /** - * @brief Implicit bool conversion for error checking. - * - * @return true if this represents an error. - * - * @since 0.1 - */ - explicit operator bool() const noexcept { return kind != Kind::NotFound; } -}; - -// ============================================================================= -// StaticToken - Compile-Time Type-Safe Token -// ============================================================================= - -/** - * @brief Compile-time type-safe token with zero runtime overhead. - * - * StaticToken provides compile-time type safety and hash-based lookup. - * The token type and hash are template parameters, enabling complete - * compiler optimization. - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * - * @ingroup ui_core - * - * @note Thread-safe for all operations via TokenRegistry's shared_mutex. - * - * @warning Do not use the same Hash value with different T types. - * - * @code - * // Define a token at compile time - * using CounterToken = StaticToken; - * - * // Register and use - * TokenRegistry::get().register_token(42); - * auto result = CounterToken::get(); - * if (result) { int value = *result; } - * @endcode - */ -template class StaticToken { - public: - /// @brief Value type stored in this token. - using value_type = T; - - /// @brief Compile-time hash of the token name. - static constexpr uint64_t hash_value = Hash; - - /// @brief Default constructor. - StaticToken() = default; - - /// @brief Copy constructor is deleted. - StaticToken(const StaticToken&) = delete; - - /// @brief Copy assignment operator is deleted. - StaticToken& operator=(const StaticToken&) = delete; - - /** - * @brief Type-safe value accessor for registry. - * - * @tparam T Value type stored in this token. - * @return cf::expected containing pointer to the token's value or TokenError. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static cf::expected get(); - - /** - * @brief Const version of value accessor. - * - * @tparam T Value type stored in this token. - * @return cf::expected containing const pointer to the token's value or TokenError. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static cf::expected get_const(); -}; - -// ============================================================================= -// TokenRegistry - Shared-Mutex Protected Storage -// ============================================================================= - -namespace detail { - -/** - * @brief Storage slot for a single token entry. - * - * @ingroup ui_core - * @internal - */ -struct TokenSlot { - std::unique_ptr data; ///< Type-erased token data. - const std::type_info* type_info = nullptr; ///< Type identifier for validation. - std::string name; ///< Human-readable name (debug). - - TokenSlot() = default; - TokenSlot(const TokenSlot&) = delete; - TokenSlot& operator=(const TokenSlot&) = delete; - - TokenSlot(TokenSlot&& other) noexcept - : data(std::move(other.data)), type_info(other.type_info), name(std::move(other.name)) {} - - TokenSlot& operator=(TokenSlot&& other) noexcept { - if (this != &other) { - data = std::move(other.data); - type_info = other.type_info; - name = std::move(other.name); - } - return *this; - } -}; - -} // namespace detail - -/** - * @brief Thread-safe token registry. - * - * Uses std::shared_mutex to allow concurrent reads while serialising writes. - * All get() calls hold the shared lock for the duration of the access, - * which prevents use-after-free when a concurrent writer calls remove(). - * - * @ingroup ui_core - * - * @note Thread-safe for all operations. - * - * @warning Token registration is relatively expensive; design for - * infrequent writes. - * - * @code - * auto& registry = TokenRegistry::get(); - * registry.register_token>(42); - * registry.register_dynamic("userId", 12345); - * auto result = registry.get_dynamic("userId"); - * if (result) { int id = *result; } - * @endcode - */ -class TokenRegistry { - public: - /** - * @brief Result type for token operations. - */ - template using Result = cf::expected; - - /** - * @brief Gets the singleton registry instance. - * - * @return Reference to the singleton registry instance. - * @throws None - * @note Thread-safe singleton initialization. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ - static TokenRegistry& get(); - - // Non-copyable, non-movable - TokenRegistry(const TokenRegistry&) = delete; - TokenRegistry& operator=(const TokenRegistry&) = delete; - TokenRegistry(TokenRegistry&&) = delete; - TokenRegistry& operator=(TokenRegistry&&) = delete; - - /** - * @brief Registers a static token with a value. - * - * @tparam TokenToken StaticToken type (contains T and Hash). - * @tparam Args Constructor argument types. - * @param args Arguments to construct the value. - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_token(Args&&... args); - - /** - * @brief Gets a static token's value. - * - * @tparam TokenToken StaticToken type. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get(); - - /** - * @brief Gets a static token's value (const). - * - * @tparam TokenToken StaticToken type. - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_const() const; - - /** - * @brief Registers a dynamic token (forwarding constructor). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template - Result register_dynamic(std::string_view name, Args&&... args); - - /** - * @brief Registers a dynamic token (copy). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_dynamic(std::string_view name, const T& value); - - /** - * @brief Registers a dynamic token (move). - * @return Result containing void or TokenError::AlreadyRegistered. - */ - template Result register_dynamic(std::string_view name, T&& value); - - /** - * @brief Gets a dynamic token's value. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic(std::string_view name); - - /** - * @brief Gets a dynamic token's value by hash. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic_by_hash(uint64_t hash); - - /** - * @brief Gets a dynamic token's value (const). - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_dynamic_const(std::string_view name) const; - - /** - * @brief Checks if a token exists by hash. - * @return true if the token exists, false otherwise. - */ - bool contains(uint64_t hash) const noexcept; - - /** - * @brief Checks if a dynamic token exists by name. - * @return true if the token exists, false otherwise. - */ - bool contains(std::string_view name) const noexcept; - - /** - * @brief Removes a token from the registry by hash. - * @return true if removed, false if not found. - */ - bool remove(uint64_t hash); - - /** - * @brief Removes a dynamic token from the registry by name. - * @return true if removed, false if not found. - */ - bool remove(std::string_view name); - - /** - * @brief Gets the number of registered tokens. - * @return Number of tokens in the registry. - */ - size_t size() const noexcept; - - private: - TokenRegistry() = default; - ~TokenRegistry() = default; - - // Internal helpers — caller must already hold the appropriate lock. - // Returns nullptr if not found. Lock must be held by caller. - detail::TokenSlot* find_slot_locked(uint64_t hash); - const detail::TokenSlot* find_slot_locked(uint64_t hash) const; - - // Shared implementation for get_dynamic / get_dynamic_by_hash (non-const). - template Result get_by_hash_impl(uint64_t hash, const std::string& name_hint); - - // Shared implementation for get_dynamic_const. - template - Result get_by_hash_impl_const(uint64_t hash, const std::string& name_hint) const; - - mutable std::shared_mutex registry_mutex_; - std::unordered_map slot_map_; -}; - -// ============================================================================= -// Inline Implementations - StaticToken -// ============================================================================= - -/** - * @brief Type-safe value accessor for registry (implementation). - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * @return cf::expected containing pointer to the token's value or TokenError. - * @throws None - * @note Delegates to TokenRegistry::get(). - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -template -auto StaticToken::get() -> cf::expected { - return TokenRegistry::get().get>(); -} - -/** - * @brief Const version of value accessor (implementation). - * - * @tparam T Value type stored in this token. - * @tparam Hash Compile-time hash of the token name. - * @return cf::expected containing const pointer to the token's value or TokenError. - * @throws None - * @note Delegates to TokenRegistry::get_const(). - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -template -auto StaticToken::get_const() -> cf::expected { - return TokenRegistry::get().get_const>(); -} - -// ============================================================================= -// Inline Implementations - TokenRegistry -// ============================================================================= - -/** - * @brief Gets the singleton TokenRegistry instance. - * - * @return Reference to the singleton registry instance. - * @throws None - * @note Thread-safe singleton initialization. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline TokenRegistry& TokenRegistry::get() { - static TokenRegistry instance; - return instance; -} - -/** - * @brief Gets the number of registered tokens. - * - * @return Number of tokens in the registry. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline size_t TokenRegistry::size() const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.size(); -} - -/** - * @brief Checks if a token exists by hash. - * - * @param[in] hash Hash value of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::contains(uint64_t hash) const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.find(hash) != slot_map_.end(); -} - -/** - * @brief Checks if a token exists by name. - * - * @param[in] name Name of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::contains(std::string_view name) const noexcept { - return contains(cf::hash::fnv1a64(name)); -} - -/** - * @brief Removes a token from the registry by hash. - * - * @param[in] hash Hash value of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::remove(uint64_t hash) { - std::unique_lock lock(registry_mutex_); - return slot_map_.erase(hash) > 0; -} - -/** - * @brief Removes a token from the registry by name. - * - * @param[in] name Name of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool TokenRegistry::remove(std::string_view name) { - return remove(cf::hash::fnv1a64(name)); -} - -// These helpers are only called while the caller already holds the mutex. -inline detail::TokenSlot* TokenRegistry::find_slot_locked(uint64_t hash) { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -inline const detail::TokenSlot* TokenRegistry::find_slot_locked(uint64_t hash) const { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -// ----------------------------------------------------------------------------- -// Static Token Registration -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::register_token(Args&&... args) -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected( - TokenError{TokenError::Kind::AlreadyRegistered, - "Token already registered, hash: " + std::to_string(hash)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = "static_token_" + std::to_string(hash); - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -// ----------------------------------------------------------------------------- -// Static Token Get -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::get() -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - // Hold shared lock for the entire operation to prevent remove() from - // destroying the slot while reading from it. - std::shared_lock lock(registry_mutex_); - - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected(TokenError{TokenError::Kind::NotFound, - "Token not found, hash: " + std::to_string(hash)}); - } - - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected(TokenError{TokenError::Kind::Empty, "Token has no value"}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token"}); - } - - return std::any_cast(a); -} - -template -auto TokenRegistry::get_const() const -> Result { - using T = typename TokenToken::value_type; - constexpr uint64_t hash = TokenToken::hash_value; - - std::shared_lock lock(registry_mutex_); - - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected(TokenError{TokenError::Kind::NotFound, - "Token not found, hash: " + std::to_string(hash)}); - } - - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected(TokenError{TokenError::Kind::Empty, "Token has no value"}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token"}); - } - - return std::any_cast(a); -} - -// ----------------------------------------------------------------------------- -// Dynamic Token Registration -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::register_dynamic(std::string_view name, Args&&... args) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto TokenRegistry::register_dynamic(std::string_view name, const T& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(value); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto TokenRegistry::register_dynamic(std::string_view name, T&& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - - std::unique_lock lock(registry_mutex_); - - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - - detail::TokenSlot slot; - slot.data = std::make_unique(std::forward(value)); - slot.type_info = &typeid(T); - slot.name = name; - - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -// ----------------------------------------------------------------------------- -// Dynamic Token Get — shared impl -// ----------------------------------------------------------------------------- - -template -auto TokenRegistry::get_by_hash_impl(uint64_t hash, const std::string& name_hint) -> Result { - // Lock held by callers (get_dynamic / get_dynamic_by_hash) for entire scope. - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - - return std::any_cast(a); -} - -template -auto TokenRegistry::get_by_hash_impl_const(uint64_t hash, - const std::string& name_hint) const -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - - return std::any_cast(a); -} - -template auto TokenRegistry::get_dynamic(std::string_view name) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(cf::hash::fnv1a64(name), std::string(name)); -} - -template auto TokenRegistry::get_dynamic_by_hash(uint64_t hash) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(hash, std::to_string(hash)); -} - -template -auto TokenRegistry::get_dynamic_const(std::string_view name) const -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl_const(cf::hash::fnv1a64(name), std::string(name)); -} - -// ============================================================================= -// EmbeddedTokenRegistry - Non-Singleton Embeddable Registry -// ============================================================================= - -/** - * @brief Embeddable token registry for component-scoped token storage. - * - * Provides the same API as TokenRegistry but without singleton semantics. - * Can be embedded in other classes for independent token management. - * - * @ingroup ui_core - * - * @note Thread-safe for all operations. - * - * @code - * class MyComponent { - * public: - * EmbeddedTokenRegistry registry; - * void init() { registry.register_dynamic("counter", 0); } - * }; - * @endcode - */ -class EmbeddedTokenRegistry { - public: - template using Result = cf::expected; - - EmbeddedTokenRegistry() = default; - ~EmbeddedTokenRegistry() = default; - - EmbeddedTokenRegistry(const EmbeddedTokenRegistry&) = delete; - EmbeddedTokenRegistry& operator=(const EmbeddedTokenRegistry&) = delete; - - EmbeddedTokenRegistry(EmbeddedTokenRegistry&& other) noexcept - : slot_map_(std::move(other.slot_map_)) {} - - EmbeddedTokenRegistry& operator=(EmbeddedTokenRegistry&& other) noexcept { - if (this != &other) - slot_map_ = std::move(other.slot_map_); - return *this; - } - - template - Result register_dynamic(std::string_view name, Args&&... args); - - template Result register_dynamic(std::string_view name, const T& value); - template Result register_dynamic(std::string_view name, T&& value); - - /** - * @brief Gets a dynamic token's value by name. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic(std::string_view name); - /** - * @brief Gets a dynamic token's value by hash. - * @return Result containing pointer to the token's value or TokenError. - */ - template Result get_dynamic_by_hash(uint64_t hash); - /** - * @brief Gets a dynamic token's value by name (const). - * @return Result containing const pointer to the token's value or TokenError. - */ - template Result get_dynamic_const(std::string_view name) const; - - /** - * @brief Checks if a token exists by hash. - * @return true if the token exists, false otherwise. - */ - bool contains(uint64_t hash) const noexcept; - /** - * @brief Checks if a token exists by name. - * @return true if the token exists, false otherwise. - */ - bool contains(std::string_view name) const noexcept; - /** - * @brief Removes a token from the registry by hash. - * @return true if the token was removed, false if not found. - */ - bool remove(uint64_t hash); - /** - * @brief Removes a token from the registry by name. - * @return true if the token was removed, false if not found. - */ - bool remove(std::string_view name); - /** - * @brief Gets the number of registered tokens. - * @return Number of tokens in the registry. - */ - size_t size() const noexcept; - - private: - detail::TokenSlot* find_slot_locked(uint64_t hash); - const detail::TokenSlot* find_slot_locked(uint64_t hash) const; - - template Result get_by_hash_impl(uint64_t hash, const std::string& name_hint); - - template - Result get_by_hash_impl_const(uint64_t hash, const std::string& name_hint) const; - - mutable std::shared_mutex registry_mutex_; - std::unordered_map slot_map_; -}; - -// ============================================================================= -// Inline Implementations - EmbeddedTokenRegistry -// ============================================================================= - -/** - * @brief Gets the number of registered tokens. - * - * @return Number of tokens in the registry. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline size_t EmbeddedTokenRegistry::size() const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.size(); -} - -/** - * @brief Checks if a token exists by hash. - * - * @param[in] hash Hash value of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::contains(uint64_t hash) const noexcept { - std::shared_lock lock(registry_mutex_); - return slot_map_.find(hash) != slot_map_.end(); -} - -/** - * @brief Checks if a token exists by name. - * - * @param[in] name Name of the token to check. - * @return true if the token exists, false otherwise. - * @throws None - * @note Thread-safe for concurrent reads. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::contains(std::string_view name) const noexcept { - return contains(cf::hash::fnv1a64(name)); -} - -/** - * @brief Removes a token from the registry by hash. - * - * @param[in] hash Hash value of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::remove(uint64_t hash) { - std::unique_lock lock(registry_mutex_); - return slot_map_.erase(hash) > 0; -} - -/** - * @brief Removes a token from the registry by name. - * - * @param[in] name Name of the token to remove. - * @return true if the token was removed, false if not found. - * @throws None - * @note Thread-safe for exclusive access. - * @warning None - * @since 0.1 - * @ingroup ui_core - */ -inline bool EmbeddedTokenRegistry::remove(std::string_view name) { - return remove(cf::hash::fnv1a64(name)); -} - -inline detail::TokenSlot* EmbeddedTokenRegistry::find_slot_locked(uint64_t hash) { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -inline const detail::TokenSlot* EmbeddedTokenRegistry::find_slot_locked(uint64_t hash) const { - auto it = slot_map_.find(hash); - return (it != slot_map_.end()) ? &it->second : nullptr; -} - -template auto -EmbeddedTokenRegistry::register_dynamic(std::string_view name, Args&&... args) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(T(std::forward(args)...)); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template auto EmbeddedTokenRegistry::register_dynamic(std::string_view name, - const T& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(value); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template -auto EmbeddedTokenRegistry::register_dynamic(std::string_view name, T&& value) -> Result { - uint64_t hash = cf::hash::fnv1a64(name); - std::unique_lock lock(registry_mutex_); - if (slot_map_.find(hash) != slot_map_.end()) { - return cf::unexpected(TokenError{TokenError::Kind::AlreadyRegistered, - "Already registered: " + std::string(name)}); - } - detail::TokenSlot slot; - slot.data = std::make_unique(std::forward(value)); - slot.type_info = &typeid(T); - slot.name = name; - slot_map_.emplace(hash, std::move(slot)); - return {}; -} - -template auto -EmbeddedTokenRegistry::get_by_hash_impl(uint64_t hash, const std::string& name_hint) -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - return std::any_cast(a); -} - -template auto EmbeddedTokenRegistry::get_by_hash_impl_const( - uint64_t hash, const std::string& name_hint) const -> Result { - const detail::TokenSlot* slot = find_slot_locked(hash); - if (!slot) { - return cf::unexpected( - TokenError{TokenError::Kind::NotFound, "Token not found: " + name_hint}); - } - const std::any* a = slot->data.get(); - if (!a || !a->has_value()) { - return cf::unexpected( - TokenError{TokenError::Kind::Empty, "Token has no value: " + name_hint}); - } - if (slot->type_info != &typeid(T)) { - return cf::unexpected( - TokenError{TokenError::Kind::TypeMismatch, "Type mismatch for token: " + name_hint}); - } - return std::any_cast(a); -} - -template auto EmbeddedTokenRegistry::get_dynamic(std::string_view name) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(cf::hash::fnv1a64(name), std::string(name)); -} - -template auto EmbeddedTokenRegistry::get_dynamic_by_hash(uint64_t hash) -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl(hash, std::to_string(hash)); -} - -template -auto EmbeddedTokenRegistry::get_dynamic_const(std::string_view name) const -> Result { - std::shared_lock lock(registry_mutex_); - return get_by_hash_impl_const(cf::hash::fnv1a64(name), std::string(name)); -} - -} // namespace cf::ui::core \ No newline at end of file diff --git a/ui/widget/CMakeLists.txt b/ui/widget/CMakeLists.txt index 98b042bb1..223927064 100644 --- a/ui/widget/CMakeLists.txt +++ b/ui/widget/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(cf_ui_widget_material STATIC material/base/elevation_controller.cpp material/base/focus_ring.cpp material/base/painter_layer.cpp + material/base/material_widget_base.cpp material/widget/button/button.cpp material/widget/checkbox/checkbox.cpp material/widget/comboBox/combobox.cpp diff --git a/ui/widget/material/base/material_widget_base.cpp b/ui/widget/material/base/material_widget_base.cpp new file mode 100644 index 000000000..742cff4fb --- /dev/null +++ b/ui/widget/material/base/material_widget_base.cpp @@ -0,0 +1,95 @@ +/** + * @file material_widget_base.cpp + * @brief Common Material Design widget behavior composition helper. + * + * @ingroup ui_widget_material_base + */ + +#include "material_widget_base.h" + +#include "application_support/application.h" + +namespace cf::ui::widget::material::base { + +MaterialWidgetBase::MaterialWidgetBase(QWidget* owner, const Config& config) : m_owner(owner) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + m_stateMachine = new StateMachine(factory, owner); + + if (config.useRipple) { + m_ripple = new RippleHelper(factory, owner); + m_ripple->setMode(config.rippleMode); + connect(m_ripple, &RippleHelper::repaintNeeded, owner, + static_cast(&QWidget::update)); + } + + if (config.useElevation) { + m_elevation = new MdElevationController(factory, owner); + m_elevation->setElevation(config.initialElevation); + connect(m_elevation, &MdElevationController::pressOffsetChanged, owner, + static_cast(&QWidget::update)); + } + + if (config.useFocusIndicator) { + m_focusIndicator = new MdFocusIndicator(factory, owner); + } + + connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, owner, + static_cast(&QWidget::update)); +} + +void MaterialWidgetBase::onEnterEvent() { + m_stateMachine->onHoverEnter(); + m_owner->update(); +} + +void MaterialWidgetBase::onLeaveEvent() { + m_stateMachine->onHoverLeave(); + if (m_ripple) + m_ripple->onCancel(); + m_owner->update(); +} + +void MaterialWidgetBase::onMousePress(const QPoint& pos, const QRectF& bounds) { + m_stateMachine->onPress(pos); + if (m_ripple) + m_ripple->onPress(pos, bounds); + if (m_elevation) + m_elevation->setPressed(true); + m_owner->update(); +} + +void MaterialWidgetBase::onMouseRelease() { + m_stateMachine->onRelease(); + if (m_ripple) + m_ripple->onRelease(); + if (m_elevation) + m_elevation->setPressed(false); + m_owner->update(); +} + +void MaterialWidgetBase::onFocusIn() { + m_stateMachine->onFocusIn(); + if (m_focusIndicator) + m_focusIndicator->onFocusIn(); + m_owner->update(); +} + +void MaterialWidgetBase::onFocusOut() { + m_stateMachine->onFocusOut(); + if (m_focusIndicator) + m_focusIndicator->onFocusOut(); + m_owner->update(); +} + +void MaterialWidgetBase::onEnabledChange(bool enabled) { + if (enabled) { + m_stateMachine->onEnable(); + } else { + m_stateMachine->onDisable(); + } + m_owner->update(); +} + +} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/material_widget_base.h b/ui/widget/material/base/material_widget_base.h new file mode 100644 index 000000000..4a332971a --- /dev/null +++ b/ui/widget/material/base/material_widget_base.h @@ -0,0 +1,214 @@ +/** + * @file material_widget_base.h + * @brief Common Material Design widget behavior composition helper. + * + * Encapsulates the shared initialization, signal connections, and event + * forwarding used by all interactive Material Design 3 widgets. + * + * @ingroup ui_widget_material_base + */ + +#pragma once + +#include "base/include/base/weak_ptr/weak_ptr.h" +#include "components/material/cfmaterial_animation_factory.h" +#include "export.h" +#include "focus_ring.h" +#include "ripple_helper.h" +#include "state_machine.h" +#include "widget/material/base/elevation_controller.h" + +#include +#include +#include +#include +#include +#include + +namespace cf::ui::widget::material::base { + +/** + * @brief Composition helper for Material Design widget behavior. + * + * @details Encapsulates the common initialization, signal connections, + * and event forwarding shared by all interactive Material Design 3 + * widgets. Uses a Config struct to control which helper components + * are created. + * + * This is a "has-a" composition, not inheritance, because widgets + * inherit from different Qt base classes (QPushButton, QCheckBox, + * QSlider, etc.). + * + * @note The owner QWidget must remain valid for the lifetime of this object. + * @warning Do not use with non-interactive widgets (Label, Separator). + * @since 0.1 + * @ingroup ui_widget_material_base + * + * @code + * // In widget constructor: + * m_material(this, MaterialWidgetBase::Config{ + * .useElevation = true, + * .initialElevation = 2 + * }); + * + * // In event handlers: + * void MyWidget::enterEvent(QEnterEvent* event) { + * QPushButton::enterEvent(event); + * m_material.onEnterEvent(); + * } + * @endcode + */ + +/** + * @brief Configuration for MaterialWidgetBase helper creation. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ +struct MaterialWidgetBaseConfig { + bool useRipple = true; ///< Create RippleHelper. + bool useElevation = false; ///< Create MdElevationController. + bool useFocusIndicator = true; ///< Create MdFocusIndicator. + RippleHelper::Mode rippleMode = RippleHelper::Mode::Bounded; ///< Ripple mode. + int initialElevation = 0; ///< Initial elevation level. +}; + +class CF_UI_EXPORT MaterialWidgetBase : public QObject { + Q_OBJECT + public: + using Config = MaterialWidgetBaseConfig; + + /** + * @brief Construct and initialize helper components. + * + * @param[in] owner The parent widget (must not be null). + * @param[in] config Configuration controlling which helpers to create. + * + * @throws None + * @note Connects repaint signals to owner's update() slot. + * @warning owner must remain valid for the lifetime of this object. + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MaterialWidgetBase(QWidget* owner, const Config& config = Config{}); + + ~MaterialWidgetBase() override = default; + + // Non-copyable, movable + MaterialWidgetBase(const MaterialWidgetBase&) = delete; + MaterialWidgetBase& operator=(const MaterialWidgetBase&) = delete; + + // --- Event forwarding --- + + /** + * @brief Forward hover enter to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onEnterEvent(); + + /** + * @brief Forward hover leave to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onLeaveEvent(); + + /** + * @brief Forward mouse press to helpers and trigger repaint. + * + * @param[in] pos Mouse position relative to the widget. + * @param[in] bounds Widget bounds for ripple calculation. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onMousePress(const QPoint& pos, const QRectF& bounds); + + /** + * @brief Forward mouse release to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onMouseRelease(); + + /** + * @brief Forward focus-in to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onFocusIn(); + + /** + * @brief Forward focus-out to helpers and trigger repaint. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onFocusOut(); + + /** + * @brief Forward enabled-state change to helpers and trigger repaint. + * + * @param[in] enabled Whether the widget is now enabled. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + void onEnabledChange(bool enabled); + + // --- Accessors --- + + /** + * @brief Get the state machine helper. + * + * @return Pointer to StateMachine, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + StateMachine* stateMachine() const { return m_stateMachine; } + + /** + * @brief Get the ripple helper. + * + * @return Pointer to RippleHelper, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + RippleHelper* ripple() const { return m_ripple; } + + /** + * @brief Get the elevation controller. + * + * @return Pointer to MdElevationController, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MdElevationController* elevation() const { return m_elevation; } + + /** + * @brief Get the focus indicator. + * + * @return Pointer to MdFocusIndicator, or nullptr if not created. + * + * @since 0.1 + * @ingroup ui_widget_material_base + */ + MdFocusIndicator* focusIndicator() const { return m_focusIndicator; } + + private: + QWidget* m_owner; + StateMachine* m_stateMachine = nullptr; + RippleHelper* m_ripple = nullptr; + MdElevationController* m_elevation = nullptr; + MdFocusIndicator* m_focusIndicator = nullptr; +}; + +} // namespace cf::ui::widget::material::base diff --git a/ui/widget/material/base/state_machine.cpp b/ui/widget/material/base/state_machine.cpp index 03ab27579..73416a6ea 100644 --- a/ui/widget/material/base/state_machine.cpp +++ b/ui/widget/material/base/state_machine.cpp @@ -39,8 +39,6 @@ using namespace cf::ui::components; StateMachine::StateMachine(cf::WeakPtr factory, QObject* parent) : QObject(parent), m_state(State::StateNormal), m_opacity(0.0f) { - // WeakPtr is stored but not locked here to avoid circular dependency - // Store the WeakPtr in a member variable for later use m_animator = factory; } @@ -61,8 +59,6 @@ StateMachine::~StateMachine() { */ float StateMachine::targetOpacityForState(States s) const { // Priority order: Disabled > Pressed > Dragged > Focused > Hovered > Normal - // When multiple states are active, use the highest priority - if (s & State::StateDisabled) { return 0.0f; } @@ -79,10 +75,10 @@ float StateMachine::targetOpacityForState(States s) const { return 0.08f; } if (s & State::StateChecked) { - return 0.08f; // Checked state uses same as hovered + return 0.08f; } - return 0.0f; // StateNormal + return 0.0f; } /** @@ -99,17 +95,12 @@ void StateMachine::cancelCurrentAnimation() { if (m_currentAnimation) { auto* anim = m_currentAnimation.Get(); if (anim) { - // Disconnect all signals from this animation to this object FIRST - // This prevents any late-arriving progressChanged signals after we've stopped disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr); disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr); - // Stop the animation anim->stop(); } m_currentAnimation = nullptr; } - // Reset m_opacity to the correct target value for the current state - // This ensures we don't carry over stale progress values from cancelled animations m_opacity = targetOpacityForState(m_state); emit stateLayerOpacityChanged(m_opacity); } @@ -118,13 +109,10 @@ void StateMachine::cancelCurrentAnimation() { * @brief Slot called when the current animation finishes. * * Clears the animation reference and ensures m_opacity is set to the - * correct target value for the current state. This prevents stale - * intermediate values from being used when a new animation starts. + * correct target value for the current state. */ void StateMachine::onAnimationFinished() { m_currentAnimation = nullptr; - // Ensure m_opacity is exactly at the target value for the current state - // This guards against any rounding errors or incomplete animations float targetOpacity = targetOpacityForState(m_state); if (m_opacity != targetOpacity) { m_opacity = targetOpacity; @@ -141,7 +129,6 @@ void StateMachine::onAnimationFinished() { * @param to Target opacity value. */ void StateMachine::animateOpacityTo(float to) { - // Performance mode check: if animations are disabled, set directly auto* factory = m_animator.Get(); if (!factory || !factory->isAllEnabled()) { m_opacity = to; @@ -149,65 +136,48 @@ void StateMachine::animateOpacityTo(float to) { return; } - // CRITICAL FIX: Cancel any currently running animation before starting a new one. - // This prevents multiple animations from competing to update m_opacity, - // which causes visual glitches like flickering or incorrect opacity values - // during rapid hover enter/leave events. cancelCurrentAnimation(); - // Start animation from current opacity value float from = m_opacity; - // IMPORTANT: Use createAnimation instead of getAnimation to avoid sharing - // the same animation instance across multiple StateMachines. - // getAnimation returns a cached animation per token, which causes multiple - // StateMachines to connect their lambdas to the same animation object, - // resulting in cross-talk when any animation progresses. - // createAnimation creates a separate animation instance per call. - // See ElevationController::animatePressOffsetTo for the same pattern. - AnimationDescriptor desc( - "fade", // Animation type - "md.motion.shortEnter", // Motion spec (short duration for hover states) - "opacity", // Property (we'll override with setRange) - from, // Start value - to // End value - ); + AnimationDescriptor desc("fade", "md.motion.shortEnter", "opacity", from, to); auto anim = factory->createAnimation(desc, nullptr, this); if (!anim) { - // Fallback: direct set if animation creation fails m_opacity = to; emit stateLayerOpacityChanged(m_opacity); return; } - // Save animation reference for cancellation m_currentAnimation = anim; - - // Get raw pointer auto* rawAnim = anim.Get(); - // Connect progress signal - // Note: Qt::UniqueConnection cannot be used with lambdas, but cancelCurrentAnimation() - // disconnects all signals before starting a new animation, preventing duplicates - // - // CRITICAL: progressChanged emits 0-1 normalized progress, NOT actual opacity values. - // We must interpolate between from and to to get the actual opacity. connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this, [this, from, to](float progress) { - // progress is 0-1, interpolate to get actual opacity value m_opacity = from + (to - from) * progress; emit stateLayerOpacityChanged(m_opacity); }); - // Connect finished signal to clear the animation reference connect(rawAnim, &components::ICFAbstractAnimation::finished, this, &StateMachine::onAnimationFinished, Qt::UniqueConnection); - // Start animation rawAnim->start(components::ICFAbstractAnimation::Direction::Forward); } +// ============================================================================ +// State Transition +// ============================================================================ + +void StateMachine::transitionTo(States newState) { + if (m_state == newState) { + return; + } + States oldState = m_state; + m_state = newState; + emit stateChanged(m_state, oldState); + animateOpacityTo(targetOpacityForState(m_state)); +} + // ============================================================================ // Event Handlers // ============================================================================ @@ -215,108 +185,49 @@ void StateMachine::animateOpacityTo(float to) { void StateMachine::onHoverEnter() { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StateHovered; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateHovered); } void StateMachine::onHoverLeave() { - States oldState = m_state; - m_state &= ~static_cast(State::StateHovered); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateHovered)); } void StateMachine::onPress(const QPoint& pos) { Q_UNUSED(pos) if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StatePressed; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StatePressed); } void StateMachine::onRelease() { - States oldState = m_state; - m_state &= ~static_cast(State::StatePressed); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StatePressed)); } void StateMachine::onFocusIn() { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - m_state |= State::StateFocused; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateFocused); } void StateMachine::onFocusOut() { - States oldState = m_state; - m_state &= ~static_cast(State::StateFocused); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateFocused)); } void StateMachine::onEnable() { - States oldState = m_state; - m_state &= ~static_cast(State::StateDisabled); - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state & ~static_cast(State::StateDisabled)); } void StateMachine::onDisable() { - States oldState = m_state; - m_state |= State::StateDisabled; - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); - } + transitionTo(m_state | State::StateDisabled); } void StateMachine::onCheckedChanged(bool checked) { if (m_state & State::StateDisabled) return; - - States oldState = m_state; - if (checked) { - m_state |= State::StateChecked; + transitionTo(m_state | State::StateChecked); } else { - m_state &= ~static_cast(State::StateChecked); - } - - if (oldState != m_state) { - emit stateChanged(m_state, oldState); - animateOpacityTo(targetOpacityForState(m_state)); + transitionTo(m_state & ~static_cast(State::StateChecked)); } } diff --git a/ui/widget/material/base/state_machine.h b/ui/widget/material/base/state_machine.h index 6cc8225f2..6b22c89e8 100644 --- a/ui/widget/material/base/state_machine.h +++ b/ui/widget/material/base/state_machine.h @@ -272,6 +272,23 @@ class CF_UI_EXPORT StateMachine : public QObject { */ void cancelCurrentAnimation(); + /** + * @brief Transitions to a new state if different from current. + * + * @details Compares new state with current state. If different, + * updates the state, emits stateChanged, and animates + * opacity to the target value for the new state. + * + * @param[in] newState The target state to transition to. + * + * @throws None + * @note No-op if newState equals current state. + * @warning None + * @since N/A + * @ingroup ui_widget_material_base + */ + void transitionTo(States newState); + /** * @brief Slot called when the current animation finishes. * diff --git a/ui/widget/material/widget/button/button.cpp b/ui/widget/material/widget/button/button.cpp index bba9f39aa..a188a5994 100644 --- a/ui/widget/material/widget/button/button.cpp +++ b/ui/widget/material/widget/button/button.cpp @@ -17,12 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +30,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -46,33 +39,13 @@ using namespace cf::ui::widget::application_support; // Constructor / Destructor // ============================================================================ -Button::Button(ButtonVariant variant, QWidget* parent) : QPushButton(parent), variant_(variant) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set initial elevation based on variant - // All buttons now have elevation 2 by default for press effect - m_elevation->setElevation(2); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default font +Button::Button(ButtonVariant variant, QWidget* parent) + : QPushButton(parent), m_material(this, + MaterialWidgetBase::Config{ + .useElevation = true, + .initialElevation = 2, + }), + variant_(variant) { setFont(labelFont()); } @@ -91,71 +64,52 @@ Button::~Button() { void Button::enterEvent(QEnterEvent* event) { QPushButton::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Button::leaveEvent(QEvent* event) { QPushButton::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Button::mousePressEvent(QMouseEvent* event) { QPushButton::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - if (m_elevation && m_pressEffectEnabled) - m_elevation->setPressed(true); - update(); + if (m_pressEffectEnabled) { + m_material.onMousePress(event->pos(), rect()); + } else { + m_material.stateMachine()->onPress(event->pos()); + if (m_material.ripple()) + m_material.ripple()->onPress(event->pos(), rect()); + update(); + } } void Button::mouseReleaseEvent(QMouseEvent* event) { QPushButton::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation && m_pressEffectEnabled) - m_elevation->setPressed(false); - update(); + if (m_pressEffectEnabled) { + m_material.onMouseRelease(); + } else { + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); + update(); + } } void Button::focusInEvent(QFocusEvent* event) { QPushButton::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Button::focusOutEvent(QFocusEvent* event) { QPushButton::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Button::changeEvent(QEvent* event) { QPushButton::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -164,25 +118,25 @@ void Button::changeEvent(QEvent* event) { // ============================================================================ int Button::elevation() const { - return m_elevation ? m_elevation->elevation() : 0; + return m_material.elevation() ? m_material.elevation()->elevation() : 0; } void Button::setElevation(int level) { - if (m_elevation) { - m_elevation->setElevation(level); + if (m_material.elevation()) { + m_material.elevation()->setElevation(level); update(); } } void Button::setLightSourceAngle(float degrees) { - if (m_elevation) { - m_elevation->setLightSourceAngle(degrees); + if (m_material.elevation()) { + m_material.elevation()->setLightSourceAngle(degrees); update(); } } float Button::lightSourceAngle() const { - return m_elevation ? m_elevation->lightSourceAngle() : 15.0f; + return m_material.elevation() ? m_material.elevation()->lightSourceAngle() : 15.0f; } void Button::setLeadingIcon(const QIcon& icon) { @@ -454,8 +408,8 @@ void Button::paintEvent(QPaintEvent* event) { // 计算按压偏移 float pressOffset = 0.0f; - if (m_pressEffectEnabled && m_elevation) { - pressOffset = m_elevation->pressOffset(); + if (m_pressEffectEnabled && m_material.elevation()) { + pressOffset = m_material.elevation()->pressOffset(); } // Calculate content area (inset to make room for shadow) @@ -499,7 +453,7 @@ void Button::paintEvent(QPaintEvent* event) { // Calculate shadow margin (extra space needed for shadow) QMarginsF Button::shadowMargin() const { - if (!m_elevation || m_elevation->elevation() <= 0) { + if (!m_material.elevation() || m_material.elevation()->elevation() <= 0) { return QMarginsF(0, 0, 0, 0); } @@ -507,14 +461,14 @@ QMarginsF Button::shadowMargin() const { // 根据 elevation 级别动态计算边距 // Level 2: blur=4dp, offset=2dp, 最大偏移约 3dp // 预留边距 = offset + blur/2,更精确的阴影空间 - int level = m_elevation->elevation(); + int level = m_material.elevation()->elevation(); float margin = helper.dpToPx(2.0f + level * 1.5f); // level 2: 约 5dp return QMarginsF(margin, margin, margin, margin); } void Button::drawShadow(QPainter& p, const QRectF& contentRect, const QPainterPath& shape) { - if (m_elevation && m_elevation->elevation() > 0) { - m_elevation->paintShadow(&p, shape); + if (m_material.elevation() && m_material.elevation()->elevation() > 0) { + m_material.elevation()->paintShadow(&p, shape); } } @@ -538,11 +492,11 @@ void Button::drawBackground(QPainter& p, const QPainterPath& shape) { } void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -555,10 +509,10 @@ void Button::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void Button::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on label color (content color) - m_ripple->setColor(labelColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(labelColor()); + m_material.ripple()->paint(&p, shape); } } @@ -637,8 +591,8 @@ void Button::drawContent(QPainter& p, const QRectF& contentRect) { } void Button::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator) { - m_focusIndicator->paint(&p, shape, labelColor()); + if (m_material.focusIndicator()) { + m_material.focusIndicator()->paint(&p, shape, labelColor()); } } diff --git a/ui/widget/material/widget/button/button.h b/ui/widget/material/widget/button/button.h index 3b884af8e..79b1b8207 100644 --- a/ui/widget/material/widget/button/button.h +++ b/ui/widget/material/widget/button/button.h @@ -17,23 +17,14 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -397,11 +388,7 @@ class CF_UI_EXPORT Button : public QPushButton { QFont labelFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; ButtonVariant variant_; QIcon leadingIcon_; diff --git a/ui/widget/material/widget/checkbox/checkbox.cpp b/ui/widget/material/widget/checkbox/checkbox.cpp index 21e47f8ca..29f68b68e 100644 --- a/ui/widget/material/widget/checkbox/checkbox.cpp +++ b/ui/widget/material/widget/checkbox/checkbox.cpp @@ -17,12 +17,8 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,7 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -45,38 +40,17 @@ using namespace cf::ui::widget::material::base; // Constructor / Destructor // ============================================================================ -CheckBox::CheckBox(QWidget* parent) : QCheckBox(parent) { - // Set size policy - Preferred allows proper layout behavior +CheckBox::CheckBox(QWidget* parent) + : QCheckBox(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded (checkbox has defined bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - - // Initialize check animation based on current state - // Both PartiallyChecked and Checked use 1.0 for full mark visibility if (checkState() == Qt::Checked) { m_checkAnimationProgress = 1.0f; - m_stateMachine->onCheckedChanged(true); + m_material.stateMachine()->onCheckedChanged(true); } else if (checkState() == Qt::PartiallyChecked) { m_checkAnimationProgress = 1.0f; } - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -119,8 +93,8 @@ void CheckBox::setCheckState(Qt::CheckState state) { void CheckBox::updateAnimationProgress(float progress, bool checked) { m_checkAnimationProgress = progress; - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } update(); } @@ -128,23 +102,21 @@ void CheckBox::updateAnimationProgress(float progress, bool checked) { void CheckBox::startCheckMarkAnimation(float target) { float fromValue = m_checkAnimationProgress; - if (!m_animationFactory) { - // No factory, set directly + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_checkAnimationProgress = target; update(); return; } - // Create property animation - auto anim = m_animationFactory->createPropertyAnimation( - &m_checkAnimationProgress, fromValue, target, 300, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_checkAnimationProgress, fromValue, target, 300, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(fromValue, target); } anim->start(); @@ -164,78 +136,46 @@ CheckBox::~CheckBox() { void CheckBox::enterEvent(QEnterEvent* event) { QCheckBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void CheckBox::leaveEvent(QEvent* event) { QCheckBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void CheckBox::mousePressEvent(QMouseEvent* event) { QCheckBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), checkboxRect()); - update(); + m_material.onMousePress(event->pos(), checkboxRect()); } void CheckBox::mouseReleaseEvent(QMouseEvent* event) { QCheckBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void CheckBox::focusInEvent(QFocusEvent* event) { QCheckBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void CheckBox::focusOutEvent(QFocusEvent* event) { QCheckBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void CheckBox::changeEvent(QEvent* event) { QCheckBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void CheckBox::nextCheckState() { - // Animate from current state to new state Qt::CheckState oldState = checkState(); QCheckBox::nextCheckState(); Qt::CheckState newState = checkState(); - // Determine target progress based on new state - // Both PartiallyChecked and Checked use 1.0 for full mark visibility float newTarget = 0.0f; bool checked = false; @@ -254,18 +194,14 @@ void CheckBox::nextCheckState() { break; } - // Update state machine checked state - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } - // Start 1-second check mark animation startCheckMarkAnimation(newTarget); } bool CheckBox::hitButton(const QPoint& pos) const { - // For custom-drawn checkbox, entire widget area is clickable - // This ensures proper click handling even when text is empty return rect().contains(pos); } @@ -292,13 +228,6 @@ void CheckBox::setError(bool error) { QSize CheckBox::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design checkbox specifications: - // - Checkbox size: 18dp - // - Spacing between checkbox and text: 12dp - // - Left padding (for focus indicator): 12dp - // - Right padding: 12dp - // - Touch target: 48dp minimum - float leftPadding = helper.dpToPx(12.0f); float rightPadding = helper.dpToPx(12.0f); float boxSize = helper.dpToPx(18.0f); @@ -306,8 +235,6 @@ QSize CheckBox::sizeHint() const { float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); float width = leftPadding + boxSize + spacing + textWidth + rightPadding; - - // Ensure minimum 48dp width for easy clicking (even without text) float minWidth = helper.dpToPx(48.0f); width = std::max(width, minWidth); @@ -317,8 +244,6 @@ QSize CheckBox::sizeHint() const { } QSize CheckBox::minimumSizeHint() const { - // Return the same as sizeHint to prevent text truncation - // This ensures the checkbox always has enough space for its text return sizeHint(); } @@ -327,7 +252,6 @@ QSize CheckBox::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { return CFColor(103, 80, 164); } // Purple 700 @@ -356,7 +280,6 @@ CFColor CheckBox::checkmarkColor() const { return CFColor(colorScheme.queryColor(ERROR)); } - // Text uses PRIMARY for checked/indeterminate, ON_SURFACE for unchecked if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -366,11 +289,9 @@ CFColor CheckBox::checkmarkColor() const { } } -// Color for the check mark/indeterminate line (drawn on colored background) CFColor CheckBox::markDrawColor() const { auto* app = application_support::Application::instance(); if (!app) { - // White mark on primary background return CFColor(255, 255, 255); } @@ -397,7 +318,6 @@ CFColor CheckBox::borderColor() const { return CFColor(colorScheme.queryColor(ERROR)); } - // Checked/indeterminate uses primary, unchecked uses outline if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -411,7 +331,6 @@ CFColor CheckBox::borderColor() const { CFColor CheckBox::backgroundColor() const { auto* app = application_support::Application::instance(); if (!app) { - // No background for unchecked return CFColor(Qt::transparent); } @@ -419,7 +338,6 @@ CFColor CheckBox::backgroundColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Only checked/indeterminate has background if (isChecked() || checkState() == Qt::PartiallyChecked) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -430,24 +348,20 @@ CFColor CheckBox::backgroundColor() const { } CFColor CheckBox::stateLayerColor() const { - // State layer uses the same color as the checkbox return checkmarkColor(); } float CheckBox::cornerRadius() const { - // Checkbox uses small corner radius (2dp) CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(2.0f); } float CheckBox::checkboxSize() const { - // Material Design checkbox is 18dp CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(18.0f); } float CheckBox::strokeWidth() const { - // Border stroke width is 2dp CanvasUnitHelper helper(qApp->devicePixelRatio()); return helper.dpToPx(2.0f); } @@ -459,11 +373,8 @@ float CheckBox::strokeWidth() const { QRectF CheckBox::checkboxRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Calculate vertical centering float boxSize = checkboxSize(); float y = (height() - boxSize) / 2.0f; - - // Add padding from left (for focus indicator spacing) float x = helper.dpToPx(12.0f); return QRectF(x, y, boxSize, boxSize); @@ -501,15 +412,12 @@ void CheckBox::paintEvent(QPaintEvent* event) { drawBorder(p, box); // Step 3: Draw check mark or indeterminate mark - // Draw based on actual checkState, not animation progress - // This ensures visual consistency even during animation Qt::CheckState state = checkState(); if (state == Qt::PartiallyChecked) { drawIndeterminateMark(p, box); } else if (state == Qt::Checked) { drawCheckMark(p, box); } - // Unchecked: no mark to draw // Step 4: Draw ripple drawRipple(p, box); @@ -529,13 +437,12 @@ void CheckBox::paintEvent(QPaintEvent* event) { void CheckBox::drawBackground(QPainter& p, const QRectF& rect) { if (checkState() == Qt::Unchecked) { - return; // No background for unchecked + return; } CFColor bgColor = backgroundColor(); QColor color = bgColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -548,20 +455,17 @@ void CheckBox::drawBorder(QPainter& p, const QRectF& rect) { CFColor bColor = borderColor(); QColor color = bColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); stateQColor.setAlphaF(opacity); - // Blend state color with border color int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); @@ -571,7 +475,6 @@ void CheckBox::drawBorder(QPainter& p, const QRectF& rect) { p.save(); - // Create inset path for border float inset = strokeWidth() / 2.0f; QRectF insetRect = rect.adjusted(inset, inset, -inset, -inset); float adjustedRadius = std::max(0.0f, cornerRadius() - inset); @@ -600,14 +503,9 @@ void CheckBox::drawCheckMark(QPainter& p, const QRectF& rect) { pen.setJoinStyle(Qt::RoundJoin); p.setPen(pen); - // Draw check mark with animation progress - // The check mark has two segments: - // 1. From bottom-left to center (0.0 to 0.5 progress) - // 2. From center to top-right (0.5 to 1.0 progress) float w = rect.width(); float h = rect.height(); - // Define check mark points (relative to rect, with margins) float left = rect.left() + w * 0.25f; float bottom = rect.top() + h * 0.55f; @@ -617,20 +515,17 @@ void CheckBox::drawCheckMark(QPainter& p, const QRectF& rect) { float right = rect.left() + w * 0.75f; float top = rect.top() + h * 0.35f; - // Get animation progress float progress = m_checkAnimationProgress; - // Draw the first segment (bottom-left to center) if (progress > 0.0f) { - float segment1Progress = std::min(progress * 2.0f, 1.0f); // 0.0-0.5 maps to 0.0-1.0 + float segment1Progress = std::min(progress * 2.0f, 1.0f); float currentX = left + (centerX - left) * segment1Progress; float currentY = bottom + (centerY - bottom) * segment1Progress; p.drawLine(QPointF(left, bottom), QPointF(currentX, currentY)); } - // Draw the second segment (center to top-right) if (progress > 0.5f) { - float segment2Progress = (progress - 0.5f) * 2.0f; // 0.5-1.0 maps to 0.0-1.0 + float segment2Progress = (progress - 0.5f) * 2.0f; float currentX = centerX + (right - centerX) * segment2Progress; float currentY = centerY + (top - centerY) * segment2Progress; p.drawLine(QPointF(centerX, centerY), QPointF(currentX, currentY)); @@ -652,8 +547,6 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { pen.setCapStyle(Qt::RoundCap); p.setPen(pen); - // Indeterminate mark is a horizontal line at center - // Animated from left to right using the same progress as check mark float w = rect.width(); float h = rect.height(); @@ -662,10 +555,8 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { float x1 = rect.left() + margin; float x2 = rect.right() - margin; - // Get animation progress float progress = m_checkAnimationProgress; - // Draw animated indeterminate line (left to right) if (progress > 0.0f) { float currentX = x1 + (x2 - x1) * progress; p.drawLine(QPointF(x1, y), QPointF(currentX, y)); @@ -675,12 +566,11 @@ void CheckBox::drawIndeterminateMark(QPainter& p, const QRectF& rect) { } void CheckBox::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, cornerRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } @@ -695,23 +585,20 @@ void CheckBox::drawText(QPainter& p, const QRectF& rect) { p.setPen(textColor.native_color()); } - // Use widget's font p.setFont(font()); - // Draw text vertically centered, left aligned - QRectF textBounds = rect.adjusted(0, 2, 0, -2); // Small adjustment for visual centering + QRectF textBounds = rect.adjusted(0, 2, 0, -2); p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); } void CheckBox::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { - // Expand rect slightly for focus ring + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(4.0f); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, shape, checkmarkColor()); + m_material.focusIndicator()->paint(&p, shape, checkmarkColor()); } } diff --git a/ui/widget/material/widget/checkbox/checkbox.h b/ui/widget/material/widget/checkbox/checkbox.h index 36ac08d5b..0046ae386 100644 --- a/ui/widget/material/widget/checkbox/checkbox.h +++ b/ui/widget/material/widget/checkbox/checkbox.h @@ -1,351 +1,340 @@ -/** - * @file ui/widget/material/widget/checkbox/checkbox.h - * @brief Material Design 3 CheckBox widget. - * - * Implements Material Design 3 checkbox with support for unchecked, checked, - * and indeterminate states. Includes ripple effects, state layers, and focus - * indicators following Material Design 3 specifications. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" -#include -#include - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 CheckBox widget. - * - * @details Implements Material Design 3 checkbox with support for unchecked, - * checked, and indeterminate states. Includes ripple effects, state - * layers, and focus indicators following Material Design 3 - * specifications. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT CheckBox : public QCheckBox { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text CheckBox text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit CheckBox(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~CheckBox() override; - - /** - * @brief Sets the checked state. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Sets the check state. - * - * @param[in] state The check state to set. - * - * @throws None - * @note Updates animation progress for correct rendering. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setCheckState(Qt::CheckState state); - - /** - * @brief Gets whether the checkbox is in error state. - * - * @return true if error state is active, false otherwise. - * - * @throws None - * @note Error state affects the border color. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false to clear. - * - * @throws None - * @note Error state uses error color for the border. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the checkbox. - * - * @throws None - * @note Based on icon size, text, and spacing. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when error state changes. - * - * @param[in] error The new error state. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void errorChanged(bool error); - - protected: - /** - * @brief Paints the checkbox. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] event State change event. - * - * @throws None - * @note Triggers check mark animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - /** - * @brief Determines if a point is within the clickable button area. - * - * @param[in] pos The point to check, in widget coordinates. - * @return true if the point is within the clickable area, false otherwise. - * - * @throws None - * @note Overrides QAbstractButton behavior to make entire widget clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - private: - // Drawing helpers - Material Design paint pipeline - QRectF checkboxRect() const; - QRectF textRect() const; - void drawBackground(QPainter& p, const QRectF& rect); - void drawBorder(QPainter& p, const QRectF& rect); - void drawCheckMark(QPainter& p, const QRectF& rect); - void drawIndeterminateMark(QPainter& p, const QRectF& rect); - void drawRipple(QPainter& p, const QRectF& rect); - void drawText(QPainter& p, const QRectF& rect); - void drawFocusIndicator(QPainter& p, const QRectF& rect); - - // Animation helper - void updateAnimationProgress(float progress, bool checked); - void startCheckMarkAnimation(float target); - - // Color access methods - CFColor checkmarkColor() const; - CFColor markDrawColor() const; // Color for check/indeterminate mark on background - CFColor borderColor() const; - CFColor backgroundColor() const; - CFColor stateLayerColor() const; - float cornerRadius() const; - - // Helper to get checkbox size in pixels - float checkboxSize() const; - float strokeWidth() const; - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Check mark animation progress (0.0 to 1.0) - float m_checkAnimationProgress = 0.0f; - - // Error state - bool m_error = false; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/checkbox/checkbox.h + * @brief Material Design 3 CheckBox widget. + * + * Implements Material Design 3 checkbox with support for unchecked, checked, + * and indeterminate states. Includes ripple effects, state layers, and focus + * indicators following Material Design 3 specifications. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" +#include +#include + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 CheckBox widget. + * + * @details Implements Material Design 3 checkbox with support for unchecked, + * checked, and indeterminate states. Includes ripple effects, state + * layers, and focus indicators following Material Design 3 + * specifications. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT CheckBox : public QCheckBox { + Q_OBJECT + Q_PROPERTY(bool error READ hasError WRITE setError NOTIFY errorChanged) + + public: + /** + * @brief Constructor. + * + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit CheckBox(QWidget* parent = nullptr); + + /** + * @brief Constructor with text. + * + * @param[in] text CheckBox text label. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit CheckBox(const QString& text, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~CheckBox() override; + + /** + * @brief Sets the checked state. + * + * @param[in] checked true to check, false to uncheck. + * + * @throws None + * @note Updates animation progress for correct rendering. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setChecked(bool checked); + + /** + * @brief Sets the check state. + * + * @param[in] state The check state to set. + * + * @throws None + * @note Updates animation progress for correct rendering. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setCheckState(Qt::CheckState state); + + /** + * @brief Gets whether the checkbox is in error state. + * + * @return true if error state is active, false otherwise. + * + * @throws None + * @note Error state affects the border color. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hasError() const; + + /** + * @brief Sets the error state. + * + * @param[in] error true to set error state, false to clear. + * + * @throws None + * @note Error state uses error color for the border. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setError(bool error); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the checkbox. + * + * @throws None + * @note Based on icon size, text, and spacing. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + signals: + /** + * @brief Signal emitted when error state changes. + * + * @param[in] error The new error state. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void errorChanged(bool error); + + protected: + /** + * @brief Paints the checkbox. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles mouse enter event. + * + * @param[in] event Enter event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void enterEvent(QEnterEvent* event) override; + + /** + * @brief Handles mouse leave event. + * + * @param[in] event Leave event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void leaveEvent(QEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple and press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles check state change event. + * + * @param[in] event State change event. + * + * @throws None + * @note Triggers check mark animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void nextCheckState() override; + + /** + * @brief Determines if a point is within the clickable button area. + * + * @param[in] pos The point to check, in widget coordinates. + * @return true if the point is within the clickable area, false otherwise. + * + * @throws None + * @note Overrides QAbstractButton behavior to make entire widget clickable. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hitButton(const QPoint& pos) const override; + + private: + // Drawing helpers - Material Design paint pipeline + QRectF checkboxRect() const; + QRectF textRect() const; + void drawBackground(QPainter& p, const QRectF& rect); + void drawBorder(QPainter& p, const QRectF& rect); + void drawCheckMark(QPainter& p, const QRectF& rect); + void drawIndeterminateMark(QPainter& p, const QRectF& rect); + void drawRipple(QPainter& p, const QRectF& rect); + void drawText(QPainter& p, const QRectF& rect); + void drawFocusIndicator(QPainter& p, const QRectF& rect); + + // Animation helper + void updateAnimationProgress(float progress, bool checked); + void startCheckMarkAnimation(float target); + + // Color access methods + CFColor checkmarkColor() const; + CFColor markDrawColor() const; // Color for check/indeterminate mark on background + CFColor borderColor() const; + CFColor backgroundColor() const; + CFColor stateLayerColor() const; + float cornerRadius() const; + + // Helper to get checkbox size in pixels + float checkboxSize() const; + float strokeWidth() const; + + // Behavior components + base::MaterialWidgetBase m_material; + + // Check mark animation progress (0.0 to 1.0) + float m_checkAnimationProgress = 0.0f; + + // Error state + bool m_error = false; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/comboBox/combobox.cpp b/ui/widget/material/widget/comboBox/combobox.cpp index af70e4ca3..4186ae564 100644 --- a/ui/widget/material/widget/comboBox/combobox.cpp +++ b/ui/widget/material/widget/comboBox/combobox.cpp @@ -17,12 +17,10 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -68,31 +66,19 @@ constexpr float DEFAULT_WIDTH_DP = 200.0f; // Constructor / Destructor // ============================================================================ -ComboBox::ComboBox(QWidget* parent) : QComboBox(parent), variant_(ComboBoxVariant::Filled) { +ComboBox::ComboBox(QWidget* parent) + : QComboBox(parent), variant_(ComboBoxVariant::Filled), + m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }) { // Set size policy setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Set minimum contents length for proper sizing setMinimumContentsLength(1); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Initialize popup animation m_popupAnimation = new QPropertyAnimation(this); m_popupAnimation->setEasingCurve(QEasingCurve::OutCubic); @@ -123,75 +109,50 @@ void ComboBox::setVariant(ComboBoxVariant variant) { void ComboBox::enterEvent(QEnterEvent* event) { QComboBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void ComboBox::leaveEvent(QEvent* event) { QComboBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void ComboBox::mousePressEvent(QMouseEvent* event) { QComboBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), fieldRect()); - update(); + m_material.onMousePress(event->pos(), fieldRect()); } void ComboBox::mouseReleaseEvent(QMouseEvent* event) { QComboBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void ComboBox::focusInEvent(QFocusEvent* event) { QComboBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void ComboBox::focusOutEvent(QFocusEvent* event) { QComboBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void ComboBox::changeEvent(QEvent* event) { QComboBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void ComboBox::showPopup() { + // Get animation factory locally for custom arrow animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + // Start arrow rotation animation - if (m_animationFactory) { - auto anim = m_animationFactory->createPropertyAnimation( - &m_arrowRotation, 0.0f, 180.0f, 200, cf::ui::base::Easing::Type::Standard, this); + if (factory) { + auto anim = factory->createPropertyAnimation(&m_arrowRotation, 0.0f, 180.0f, 200, + cf::ui::base::Easing::Type::Standard, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values if (auto* propAnim = @@ -266,11 +227,14 @@ void ComboBox::hidePopup() { m_popupAnimation->stop(); } + // Get animation factory locally for custom arrow animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + // Reset arrow rotation animation - if (m_animationFactory) { - auto anim = m_animationFactory->createPropertyAnimation( - &m_arrowRotation, m_arrowRotation, 0.0f, 150, cf::ui::base::Easing::Type::Standard, - this); + if (factory) { + auto anim = factory->createPropertyAnimation(&m_arrowRotation, m_arrowRotation, 0.0f, 150, + cf::ui::base::Easing::Type::Standard, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values if (auto* propAnim = @@ -524,8 +488,8 @@ void ComboBox::drawOutline(QPainter& p, const QPainterPath& shape) { // For filled variant, outline is only visible when hovered/focused if (variant_ == ComboBoxVariant::Filled) { - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { color.setAlphaF(opacity); } else { @@ -549,11 +513,11 @@ void ComboBox::drawOutline(QPainter& p, const QPainterPath& shape) { } void ComboBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!m_stateMachine || !isEnabled()) { + if (!m_material.stateMachine() || !isEnabled()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -566,11 +530,11 @@ void ComboBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void ComboBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + m_material.ripple()->setColor(stateLayerColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -624,7 +588,7 @@ void ComboBox::drawArrow(QPainter& p, const QRectF& rect) { } void ComboBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { // Expand rect slightly for focus ring CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); @@ -632,7 +596,7 @@ void ComboBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { QRectF focusRect = field.adjusted(-margin, -margin, margin, margin); QPainterPath focusShape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, focusShape, containerColor()); + m_material.focusIndicator()->paint(&p, focusShape, containerColor()); } } diff --git a/ui/widget/material/widget/comboBox/combobox.h b/ui/widget/material/widget/comboBox/combobox.h index 95c57b010..f23b13d99 100644 --- a/ui/widget/material/widget/comboBox/combobox.h +++ b/ui/widget/material/widget/comboBox/combobox.h @@ -14,22 +14,14 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -285,10 +277,7 @@ class CF_UI_EXPORT ComboBox : public QComboBox { float arrowRotation() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Variant ComboBoxVariant variant_; diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp index f054c0eb9..ab8cea448 100644 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp +++ b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp @@ -17,11 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -70,7 +64,14 @@ inline CFColor fallbackPrimary() { // ============================================================================ DoubleSpinBox::DoubleSpinBox(QWidget* parent) - : QDoubleSpinBox(parent), m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), + : QDoubleSpinBox(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = true, + .useFocusIndicator = true, + .initialElevation = 0, + }), + m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), m_pressingIncrementButton(false), m_pressingDecrementButton(false) { // Disable native frame and background @@ -89,24 +90,6 @@ DoubleSpinBox::DoubleSpinBox(QWidget* parent) lineEdit()->setAlignment(Qt::AlignRight); } - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default font setFont(textFont()); @@ -127,22 +110,16 @@ DoubleSpinBox::~DoubleSpinBox() { void DoubleSpinBox::enterEvent(QEnterEvent* event) { QDoubleSpinBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void DoubleSpinBox::leaveEvent(QEvent* event) { QDoubleSpinBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); + m_material.onLeaveEvent(); // Reset button hover states m_hoveringIncrementButton = false; m_hoveringDecrementButton = false; - update(); } void DoubleSpinBox::mousePressEvent(QMouseEvent* event) { @@ -162,11 +139,7 @@ void DoubleSpinBox::mousePressEvent(QMouseEvent* event) { } QDoubleSpinBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - update(); + m_material.onMousePress(event->pos(), rect()); } void DoubleSpinBox::mouseReleaseEvent(QMouseEvent* event) { @@ -179,11 +152,7 @@ void DoubleSpinBox::mouseReleaseEvent(QMouseEvent* event) { } QDoubleSpinBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void DoubleSpinBox::mouseMoveEvent(QMouseEvent* event) { @@ -193,32 +162,18 @@ void DoubleSpinBox::mouseMoveEvent(QMouseEvent* event) { void DoubleSpinBox::focusInEvent(QFocusEvent* event) { QDoubleSpinBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void DoubleSpinBox::focusOutEvent(QFocusEvent* event) { QDoubleSpinBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void DoubleSpinBox::changeEvent(QEvent* event) { QDoubleSpinBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); updateTextColor(); update(); } @@ -522,11 +477,11 @@ void DoubleSpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { } void DoubleSpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -539,10 +494,10 @@ void DoubleSpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void DoubleSpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on text color - m_ripple->setColor(textColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(textColor()); + m_material.ripple()->paint(&p, shape); } } @@ -692,8 +647,8 @@ void DoubleSpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { } void DoubleSpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator && hasFocus()) { - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + if (m_material.focusIndicator() && hasFocus()) { + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.h b/ui/widget/material/widget/doublespinbox/doublespinbox.h index c874676e8..41c5a1bca 100644 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.h +++ b/ui/widget/material/widget/doublespinbox/doublespinbox.h @@ -18,21 +18,13 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -268,10 +260,7 @@ class CF_UI_EXPORT DoubleSpinBox : public QDoubleSpinBox { QFont textFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Internal state bool m_hoveringIncrementButton; diff --git a/ui/widget/material/widget/listview/listview.cpp b/ui/widget/material/widget/listview/listview.cpp index 1340d2189..9679338a4 100644 --- a/ui/widget/material/widget/listview/listview.cpp +++ b/ui/widget/material/widget/listview/listview.cpp @@ -18,11 +18,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -72,8 +68,6 @@ class ListViewDelegate : public QStyledItemDelegate { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -84,33 +78,21 @@ using namespace cf::ui::widget::application_support; // ============================================================================ ListView::ListView(QWidget* parent) - : QListView(parent), m_itemHeight(ItemHeight::SingleLine), m_showSeparator(true), - m_rippleEnabled(true), m_hoveredIndex(-1), m_pressedIndex(-1), m_delegate(nullptr) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - + : QListView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_itemHeight(ItemHeight::SingleLine), m_showSeparator(true), m_rippleEnabled(true), + m_hoveredIndex(-1), m_pressedIndex(-1), m_delegate(nullptr) { // Initialize and set delegate for item size control m_delegate = new ListViewDelegate(this); setItemDelegate(m_delegate); - // Set ripple mode to bounded (clipped to item bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - // Configure viewport for mouse tracking viewport()->setMouseTracking(true); - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default selection mode setSelectionMode(QAbstractItemView::SingleSelection); setSelectionBehavior(QAbstractItemView::SelectRows); @@ -132,20 +114,12 @@ ListView::~ListView() { void ListView::enterEvent(QEnterEvent* event) { QListView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void ListView::leaveEvent(QEvent* event) { QListView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); m_hoveredIndex = -1; update(); } @@ -159,13 +133,7 @@ void ListView::mousePressEvent(QMouseEvent* event) { m_pressedIndex = index.row(); m_pressPosition = event->pos(); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } - if (m_ripple) { - QRectF itemRect = visualItemRect(index); - m_ripple->onPress(event->pos(), itemRect); - } + m_material.onMousePress(event->pos(), visualItemRect(index)); } update(); } @@ -174,12 +142,7 @@ void ListView::mouseReleaseEvent(QMouseEvent* event) { QListView::mouseReleaseEvent(event); if (m_pressedIndex >= 0) { - if (m_stateMachine) { - m_stateMachine->onRelease(); - } - if (m_ripple) { - m_ripple->onRelease(); - } + m_material.onMouseRelease(); m_pressedIndex = -1; } update(); @@ -187,37 +150,18 @@ void ListView::mouseReleaseEvent(QMouseEvent* event) { void ListView::focusInEvent(QFocusEvent* event) { QListView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void ListView::focusOutEvent(QFocusEvent* event) { QListView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void ListView::changeEvent(QEvent* event) { QListView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -579,7 +523,7 @@ void ListView::drawItemBackground(QPainter& p, const QRectF& itemRect, int index } void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } @@ -588,7 +532,7 @@ void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -602,11 +546,11 @@ void ListView::drawItemStateLayer(QPainter& p, const QRectF& itemRect, int index } void ListView::drawItemRipple(QPainter& p, const QRectF& itemRect, int index) { - if (!m_rippleEnabled || !m_ripple) { + if (!m_rippleEnabled || !m_material.ripple()) { return; } - m_ripple->setColor(textColor()); + m_material.ripple()->setColor(textColor()); // Create a clip path for the item QPainterPath clipPath; @@ -614,7 +558,7 @@ void ListView::drawItemRipple(QPainter& p, const QRectF& itemRect, int index) { p.save(); p.setClipPath(clipPath); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); p.restore(); } @@ -727,7 +671,7 @@ void ListView::drawSeparator(QPainter& p, const QRectF& itemRect) { } void ListView::drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index) { - if (!m_focusIndicator) { + if (!m_material.focusIndicator()) { return; } @@ -736,7 +680,7 @@ void ListView::drawFocusIndicator(QPainter& p, const QRectF& itemRect, int index shape.addRect(itemRect); // Use the text color as the focus indicator color - m_focusIndicator->paint(&p, shape, textColor()); + m_material.focusIndicator()->paint(&p, shape, textColor()); } } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/listview/listview.h b/ui/widget/material/widget/listview/listview.h index 4b8838153..454e58dd5 100644 --- a/ui/widget/material/widget/listview/listview.h +++ b/ui/widget/material/widget/listview/listview.h @@ -19,19 +19,11 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - class ListViewDelegate; using CFColor = cf::ui::base::CFColor; @@ -341,10 +333,7 @@ class CF_UI_EXPORT ListView : public QListView { QFont secondaryFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Item properties ItemHeight m_itemHeight; diff --git a/ui/widget/material/widget/progressbar/progressbar.cpp b/ui/widget/material/widget/progressbar/progressbar.cpp index 50d3c515c..59e6fab15 100644 --- a/ui/widget/material/widget/progressbar/progressbar.cpp +++ b/ui/widget/material/widget/progressbar/progressbar.cpp @@ -18,11 +18,10 @@ #include "base/device_pixel.h" #include "base/easing.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/state_machine.h" #include #include @@ -56,22 +55,15 @@ constexpr float FOCUS_RING_MARGIN_DP = 4.0f; // Constructor / Destructor // ============================================================================ -ProgressBar::ProgressBar(QWidget* parent) : QProgressBar(parent) { +ProgressBar::ProgressBar(QWidget* parent) + : QProgressBar(parent), m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = false, + .useElevation = false, + .useFocusIndicator = true, + }) { // Set size policy setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Connect repaint signals - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Start indeterminate animation if in indeterminate mode if (minimum() == 0 && maximum() == 0) { startIndeterminateAnimation(); @@ -89,47 +81,28 @@ ProgressBar::~ProgressBar() { void ProgressBar::enterEvent(QEnterEvent* event) { QProgressBar::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void ProgressBar::leaveEvent(QEvent* event) { QProgressBar::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - update(); + m_material.onLeaveEvent(); } void ProgressBar::focusInEvent(QFocusEvent* event) { QProgressBar::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void ProgressBar::focusOutEvent(QFocusEvent* event) { QProgressBar::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void ProgressBar::changeEvent(QEvent* event) { QProgressBar::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -253,7 +226,11 @@ QRectF ProgressBar::trackRect() const { // ============================================================================ void ProgressBar::startIndeterminateAnimation() { - if (!m_animationFactory || m_indeterminateAnimating) { + // Get animation factory locally for custom indeterminate animation + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory || m_indeterminateAnimating) { return; } @@ -261,8 +238,8 @@ void ProgressBar::startIndeterminateAnimation() { // Create a looping animation for indeterminate mode // Using a property animation on m_indeterminatePosition - auto anim = m_animationFactory->createPropertyAnimation( - &m_indeterminatePosition, 0.0f, 1.0f, 1500, cf::ui::base::Easing::Type::Linear, this); + auto anim = factory->createPropertyAnimation(&m_indeterminatePosition, 0.0f, 1.0f, 1500, + cf::ui::base::Easing::Type::Linear, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values @@ -348,8 +325,8 @@ void ProgressBar::drawFill(QPainter& p, const QRectF& trackRect) { } // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); @@ -442,13 +419,13 @@ void ProgressBar::drawText(QPainter& p, const QRectF& rect) { } void ProgressBar::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, cornerRadius() + margin); - m_focusIndicator->paint(&p, shape, fillColor()); + m_material.focusIndicator()->paint(&p, shape, fillColor()); } } diff --git a/ui/widget/material/widget/progressbar/progressbar.h b/ui/widget/material/widget/progressbar/progressbar.h index cf32149a9..05a6324ac 100644 --- a/ui/widget/material/widget/progressbar/progressbar.h +++ b/ui/widget/material/widget/progressbar/progressbar.h @@ -15,20 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -197,9 +190,7 @@ class CF_UI_EXPORT ProgressBar : public QProgressBar { void updateIndeterminatePosition(); // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Indeterminate animation state (0.0 to 1.0) float m_indeterminatePosition = 0.0f; diff --git a/ui/widget/material/widget/radiobutton/radiobutton.cpp b/ui/widget/material/widget/radiobutton/radiobutton.cpp index 125e798dd..573e0d209 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.cpp +++ b/ui/widget/material/widget/radiobutton/radiobutton.cpp @@ -16,12 +16,8 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/easing.h" -#include "cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -33,7 +29,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -42,14 +37,12 @@ using namespace cf::ui::widget::application_support; // Material Design 3 specifications for RadioButton namespace { -// Radio button size specifications (in dp) constexpr float RADIO_SIZE_DP = 20.0f; // Outer ring diameter constexpr float INNER_CIRCLE_SIZE_DP = 10.0f; // Inner circle diameter (50% of outer) constexpr float TOUCH_TARGET_DP = 48.0f; // Minimum touch target size constexpr float TEXT_SPACING_DP = 8.0f; // Spacing between radio and text constexpr float STROKE_WIDTH_DP = 2.0f; // Outer ring stroke width -// Inner circle animation scale values constexpr float INNER_CIRCLE_SCALE_UNCHECKED = 0.0f; constexpr float INNER_CIRCLE_SCALE_CHECKED = 1.0f; } // namespace @@ -77,29 +70,9 @@ inline CFColor fallbackError() { // Constructor / Destructor // ============================================================================ -RadioButton::RadioButton(QWidget* parent) : QRadioButton(parent) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded (clipped by widget bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - - // Set default font +RadioButton::RadioButton(QWidget* parent) + : QRadioButton(parent), m_material(this, MaterialWidgetBase::Config{.useElevation = false}) { setFont(labelFont()); - - // Initialize inner circle scale based on checked state m_innerCircleScale = isChecked() ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; } @@ -117,80 +90,51 @@ RadioButton::~RadioButton() { void RadioButton::enterEvent(QEnterEvent* event) { QRadioButton::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void RadioButton::leaveEvent(QEvent* event) { QRadioButton::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } - update(); + m_material.onLeaveEvent(); } void RadioButton::mousePressEvent(QMouseEvent* event) { QRadioButton::mousePressEvent(event); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } - if (m_ripple && m_pressEffectEnabled) { - // Calculate radio rect for ripple clipping + if (m_pressEffectEnabled) { QRectF radioRect = calculateRadioRect(); - m_ripple->onPress(event->pos(), radioRect.united(calculateTextRect(radioRect))); + m_material.onMousePress(event->pos(), radioRect.united(calculateTextRect(radioRect))); + } else { + m_material.stateMachine()->onPress(event->pos()); + update(); } - update(); } void RadioButton::mouseReleaseEvent(QMouseEvent* event) { QRadioButton::mouseReleaseEvent(event); - if (m_stateMachine) { - m_stateMachine->onRelease(); - } - if (m_ripple && m_pressEffectEnabled) { - m_ripple->onRelease(); + if (m_pressEffectEnabled) { + m_material.onMouseRelease(); + } else { + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); + update(); } - update(); } void RadioButton::focusInEvent(QFocusEvent* event) { QRadioButton::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void RadioButton::focusOutEvent(QFocusEvent* event) { QRadioButton::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void RadioButton::changeEvent(QEvent* event) { QRadioButton::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -199,10 +143,9 @@ void RadioButton::nextCheckState() { QRadioButton::nextCheckState(); bool isCheckedNow = isChecked(); - // Update inner circle animation when checked state changes if (wasChecked != isCheckedNow) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isCheckedNow); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isCheckedNow); } startInnerCircleAnimation(isCheckedNow); } @@ -211,19 +154,16 @@ void RadioButton::nextCheckState() { void RadioButton::setChecked(bool checked) { bool wasChecked = isChecked(); if (wasChecked == checked) { - return; // No change + return; } QRadioButton::setChecked(checked); bool isCheckedNow = isChecked(); - // Sync inner circle scale with checked state - // This handles programmatic setChecked() calls which don't trigger nextCheckState if (wasChecked != isCheckedNow) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isCheckedNow); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isCheckedNow); } - // For programmatic checked change, set scale immediately without animation m_innerCircleScale = isCheckedNow ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; update(); @@ -257,8 +197,6 @@ void RadioButton::setPressEffectEnabled(bool enabled) { } bool RadioButton::hitButton(const QPoint& pos) const { - // For custom-drawn radio button, entire widget area is clickable - // This ensures proper click handling even when text is empty return rect().contains(pos); } @@ -269,23 +207,13 @@ bool RadioButton::hitButton(const QPoint& pos) const { QSize RadioButton::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design 3 specifications: - // - Radio size: 20dp - // - Spacing between radio and text: 8dp - // - Left padding (for stroke clipping): 12dp - // - Touch target: 48dp minimum - float leftPadding = helper.dpToPx(12.0f); float radioSize = helper.dpToPx(RADIO_SIZE_DP); float textSpacing = helper.dpToPx(TEXT_SPACING_DP); - // Text width float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); - // Total width float totalWidth = leftPadding + radioSize + textSpacing + textWidth; - - // Height is at least the touch target size float height = helper.dpToPx(TOUCH_TARGET_DP); return QSize(int(std::ceil(totalWidth)), int(std::ceil(height))); @@ -310,12 +238,10 @@ QSize RadioButton::minimumSizeHint() const { // ============================================================================ CFColor RadioButton::radioColor() const { - // Error state takes priority if (m_hasError) { return errorColor(); } - // Checked state uses primary, unchecked uses outline if (isChecked()) { auto* app = Application::instance(); if (!app) { @@ -361,7 +287,6 @@ CFColor RadioButton::onRadioColor() const { } CFColor RadioButton::stateLayerColor() const { - // State layer uses the same color as the radio (primary when checked, onSurface when unchecked) if (isChecked()) { return radioColor(); } else { @@ -398,7 +323,6 @@ CFColor RadioButton::errorColor() const { QFont RadioButton::labelFont() const { auto* app = Application::instance(); if (!app) { - // Fallback to system font with reasonable size QFont font = QRadioButton::font(); font.setPixelSize(14); font.setWeight(QFont::Normal); @@ -425,9 +349,6 @@ QRectF RadioButton::calculateRadioRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); float radioSize = helper.dpToPx(RADIO_SIZE_DP); float y = (height() - radioSize) / 2.0f; - - // Add left padding (for focus indicator and to prevent stroke clipping) - // This ensures the outer ring stroke is not clipped at the left edge float x = helper.dpToPx(12.0f); return QRectF(x, y, radioSize, radioSize); @@ -449,23 +370,21 @@ void RadioButton::startInnerCircleAnimation(bool checked) { float targetScale = checked ? INNER_CIRCLE_SCALE_CHECKED : INNER_CIRCLE_SCALE_UNCHECKED; float fromScale = m_innerCircleScale; - if (!m_animationFactory) { - // No factory, set directly + auto factory = cf::WeakPtr::DynamicCast( + Application::animationFactory()); + + if (!factory) { m_innerCircleScale = targetScale; update(); return; } - // Create property animation - auto anim = m_animationFactory->createPropertyAnimation( - &m_innerCircleScale, fromScale, targetScale, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_innerCircleScale, fromScale, targetScale, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(fromScale, targetScale); } anim->start(); @@ -485,7 +404,6 @@ void RadioButton::paintEvent(QPaintEvent* event) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); - // Calculate layout QRectF radioRect = calculateRadioRect(); QRectF textRect = calculateTextRect(radioRect); @@ -513,24 +431,21 @@ void RadioButton::paintEvent(QPaintEvent* event) { // ============================================================================ void RadioButton::drawStateLayer(QPainter& p, const QRectF& radioRect) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } - // MD3 spec: state layer is 40dp circle (2x radio size), centered on radio - // This corresponds to the 48dp touch target CanvasUnitHelper helper(qApp->devicePixelRatio()); float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp QPointF center = radioRect.center(); QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, stateLayerSize, stateLayerSize); - // Create circular state layer path QPainterPath circlePath; circlePath.addEllipse(stateLayerRect); @@ -542,25 +457,22 @@ void RadioButton::drawStateLayer(QPainter& p, const QRectF& radioRect) { } void RadioButton::drawRipple(QPainter& p, const QRectF& radioRect) { - if (!m_ripple || !m_pressEffectEnabled) { + if (!m_material.ripple() || !m_pressEffectEnabled) { return; } - // Set ripple color based on state - m_ripple->setColor(stateLayerColor()); + m_material.ripple()->setColor(stateLayerColor()); - // MD3 spec: ripple is clipped to 40dp circle (2x radio size), centered on radio CanvasUnitHelper helper(qApp->devicePixelRatio()); float stateLayerSize = helper.dpToPx(RADIO_SIZE_DP * 2.0f); // 40dp QPointF center = radioRect.center(); QRectF stateLayerRect(center.x() - stateLayerSize / 2.0f, center.y() - stateLayerSize / 2.0f, stateLayerSize, stateLayerSize); - // Create clipping path for the state layer circle QPainterPath clipPath; clipPath.addEllipse(stateLayerRect); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } void RadioButton::drawOuterRing(QPainter& p, const QRectF& radioRect) { @@ -570,25 +482,20 @@ void RadioButton::drawOuterRing(QPainter& p, const QRectF& radioRect) { CFColor ringColor = radioColor(); QColor color = ringColor.native_color(); - // Handle disabled state if (!isEnabled()) { - color.setAlphaF(0.38f); // 38% opacity for disabled + color.setAlphaF(0.38f); } p.save(); - // Inset the rect by half the stroke width to ensure the stroke stays within bounds - // This prevents the left edge from being clipped when x=0 float inset = strokeWidth / 2.0f; QRectF insetRect = radioRect.adjusted(inset, inset, -inset, -inset); - // Create the outer ring path from the inset rect QPainterPath ringPath; ringPath.addEllipse(insetRect); - // Set pen for stroking the ring QPen pen(color, strokeWidth); - pen.setCosmetic(false); // Use device pixels for consistent stroke width + pen.setCosmetic(false); p.setPen(pen); p.setBrush(Qt::NoBrush); @@ -602,23 +509,20 @@ void RadioButton::drawInnerCircle(QPainter& p, const QRectF& radioRect) { return; } - // Calculate inner circle dimensions based on scale float outerRadius = radioRect.width() / 2.0f; - float innerRadius = outerRadius * 0.5f; // Inner circle is 50% of outer + float innerRadius = outerRadius * 0.5f; float scaledRadius = innerRadius * m_innerCircleScale; QPointF center = radioRect.center(); p.save(); - // Create inner circle path QPainterPath innerCirclePath; innerCirclePath.addEllipse(center, scaledRadius, scaledRadius); CFColor fillColor = radioColor(); QColor color = fillColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -659,29 +563,25 @@ void RadioButton::drawText(QPainter& p, const QRectF& textRect) { p.setPen(textColor.native_color()); } - // Use label font QFont font = labelFont(); p.setFont(font); - // Draw text with vertical center alignment and left alignment p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text()); } void RadioButton::drawFocusIndicator(QPainter& p, const QRectF& radioRect) { - if (!m_focusIndicator) { + if (!m_material.focusIndicator()) { return; } - // Create focus indicator path (slightly larger than the radio button) QPainterPath focusPath; CanvasUnitHelper helper(qApp->devicePixelRatio()); - float focusPadding = helper.dpToPx(4.0f); // 4dp padding for focus ring + float focusPadding = helper.dpToPx(4.0f); focusPath.addEllipse( radioRect.adjusted(-focusPadding, -focusPadding, focusPadding, focusPadding)); - // Use the radio color for the focus indicator CFColor indicatorColor = radioColor(); - m_focusIndicator->paint(&p, focusPath, indicatorColor); + m_material.focusIndicator()->paint(&p, focusPath, indicatorColor); } } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/radiobutton/radiobutton.h b/ui/widget/material/widget/radiobutton/radiobutton.h index 5e6dfc591..714ac9f87 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.h +++ b/ui/widget/material/widget/radiobutton/radiobutton.h @@ -1,351 +1,340 @@ -/** - * @file ui/widget/material/widget/radiobutton/radiobutton.h - * @brief Material Design 3 RadioButton widget. - * - * Implements Material Design 3 radio button with circular selection area, - * inner circle scale animation, ripple effects, and focus indicators. - * Supports mutual exclusion through QButtonGroup integration. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "color.h" -#include "export.h" -#include -#include - -using CFColor = cf::ui::base::CFColor; - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -/** - * @brief Material Design 3 RadioButton widget. - * - * @details Implements Material Design 3 radio button with circular selection - * area, inner circle scale animation, ripple effects, and focus - * indicators. Supports mutual exclusion through QButtonGroup - * integration. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT RadioButton : public QRadioButton { - Q_OBJECT - Q_PROPERTY(bool error READ hasError WRITE setError DESIGNABLE true SCRIPTABLE true) - Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) - - public: - /** - * @brief Constructor. - * - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(QWidget* parent = nullptr); - - /** - * @brief Constructor with text. - * - * @param[in] text Button text label. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit RadioButton(const QString& text, QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~RadioButton() override; - - /** - * @brief Gets whether the radio button is in error state. - * - * @return true if in error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hasError() const; - - /** - * @brief Sets the error state. - * - * @param[in] error true to set error state, false otherwise. - * - * @throws None - * @note Error state displays with error color theme. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setError(bool error); - - /** - * @brief Gets whether press effect is enabled. - * - * @return true if press effect is enabled, false otherwise. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool pressEffectEnabled() const; - - /** - * @brief Sets whether press effect is enabled. - * - * @param[in] enabled true to enable press effect, false to disable. - * - * @throws None - * @note Press effect includes ripple animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPressEffectEnabled(bool enabled); - - /** - * @brief Sets the checked state of the radio button. - * - * @param[in] checked true to check, false to uncheck. - * - * @throws None - * @note Override to sync inner circle scale with checked state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setChecked(bool checked); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the radio button. - * - * @throws None - * @note Based on text and touch target requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements (48dp). - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - /** - * @brief Handles hit test for mouse events. - * - * @param[in] pos Mouse position. - * - * @return true if the position is within the clickable area. - * - * @throws None - * @note Entire widget area is clickable. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool hitButton(const QPoint& pos) const override; - - protected: - /** - * @brief Paints the radio button. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles mouse enter event. - * - * @param[in] event Enter event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void enterEvent(QEnterEvent* event) override; - - /** - * @brief Handles mouse leave event. - * - * @param[in] event Leave event. - * - * @throws None - * @note Updates hover state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void leaveEvent(QEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple and press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates press state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles check state change event. - * - * @param[in] checked Check state. - * - * @throws None - * @note Triggers inner circle scale animation. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void nextCheckState() override; - - private: - // Drawing helpers - Material Design paint pipeline - void drawStateLayer(QPainter& p, const QRectF& radioRect); - void drawRipple(QPainter& p, const QRectF& radioRect); - void drawOuterRing(QPainter& p, const QRectF& radioRect); - void drawInnerCircle(QPainter& p, const QRectF& radioRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawFocusIndicator(QPainter& p, const QRectF& radioRect); - - // Color access methods - CFColor radioColor() const; - CFColor onRadioColor() const; - CFColor stateLayerColor() const; - CFColor errorColor() const; - QFont labelFont() const; - - // Layout calculations - QRectF calculateRadioRect() const; - QRectF calculateTextRect(const QRectF& radioRect) const; - - // Animation helpers - void startInnerCircleAnimation(bool checked); - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Animation state - float m_innerCircleScale = 0.0f; - - // Properties - bool m_hasError = false; - bool m_pressEffectEnabled = true; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/radiobutton/radiobutton.h + * @brief Material Design 3 RadioButton widget. + * + * Implements Material Design 3 radio button with circular selection area, + * inner circle scale animation, ripple effects, and focus indicators. + * Supports mutual exclusion through QButtonGroup integration. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" +#include +#include + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 RadioButton widget. + * + * @details Implements Material Design 3 radio button with circular selection + * area, inner circle scale animation, ripple effects, and focus + * indicators. Supports mutual exclusion through QButtonGroup + * integration. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT RadioButton : public QRadioButton { + Q_OBJECT + Q_PROPERTY(bool error READ hasError WRITE setError DESIGNABLE true SCRIPTABLE true) + Q_PROPERTY(bool pressEffectEnabled READ pressEffectEnabled WRITE setPressEffectEnabled) + + public: + /** + * @brief Constructor. + * + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit RadioButton(QWidget* parent = nullptr); + + /** + * @brief Constructor with text. + * + * @param[in] text Button text label. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit RadioButton(const QString& text, QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~RadioButton() override; + + /** + * @brief Gets whether the radio button is in error state. + * + * @return true if in error state, false otherwise. + * + * @throws None + * @note Error state displays with error color theme. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hasError() const; + + /** + * @brief Sets the error state. + * + * @param[in] error true to set error state, false otherwise. + * + * @throws None + * @note Error state displays with error color theme. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setError(bool error); + + /** + * @brief Gets whether press effect is enabled. + * + * @return true if press effect is enabled, false otherwise. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool pressEffectEnabled() const; + + /** + * @brief Sets whether press effect is enabled. + * + * @param[in] enabled true to enable press effect, false to disable. + * + * @throws None + * @note Press effect includes ripple animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setPressEffectEnabled(bool enabled); + + /** + * @brief Sets the checked state of the radio button. + * + * @param[in] checked true to check, false to uncheck. + * + * @throws None + * @note Override to sync inner circle scale with checked state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setChecked(bool checked); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the radio button. + * + * @throws None + * @note Based on text and touch target requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements (48dp). + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + /** + * @brief Handles hit test for mouse events. + * + * @param[in] pos Mouse position. + * + * @return true if the position is within the clickable area. + * + * @throws None + * @note Entire widget area is clickable. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool hitButton(const QPoint& pos) const override; + + protected: + /** + * @brief Paints the radio button. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles mouse enter event. + * + * @param[in] event Enter event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void enterEvent(QEnterEvent* event) override; + + /** + * @brief Handles mouse leave event. + * + * @param[in] event Leave event. + * + * @throws None + * @note Updates hover state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void leaveEvent(QEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple and press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates press state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles check state change event. + * + * @param[in] checked Check state. + * + * @throws None + * @note Triggers inner circle scale animation. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void nextCheckState() override; + + private: + // Drawing helpers - Material Design paint pipeline + void drawStateLayer(QPainter& p, const QRectF& radioRect); + void drawRipple(QPainter& p, const QRectF& radioRect); + void drawOuterRing(QPainter& p, const QRectF& radioRect); + void drawInnerCircle(QPainter& p, const QRectF& radioRect); + void drawText(QPainter& p, const QRectF& textRect); + void drawFocusIndicator(QPainter& p, const QRectF& radioRect); + + // Color access methods + CFColor radioColor() const; + CFColor onRadioColor() const; + CFColor stateLayerColor() const; + CFColor errorColor() const; + QFont labelFont() const; + + // Layout calculations + QRectF calculateRadioRect() const; + QRectF calculateTextRect(const QRectF& radioRect) const; + + // Animation helpers + void startInnerCircleAnimation(bool checked); + + // Behavior components + base::MaterialWidgetBase m_material; + + // Animation state + float m_innerCircleScale = 0.0f; + + // Properties + bool m_hasError = false; + bool m_pressEffectEnabled = true; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/slider/slider.cpp b/ui/widget/material/widget/slider/slider.cpp index 75de40059..6da1aef2b 100644 --- a/ui/widget/material/widget/slider/slider.cpp +++ b/ui/widget/material/widget/slider/slider.cpp @@ -16,12 +16,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -34,8 +29,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -45,7 +38,6 @@ using namespace cf::ui::widget::material::base; // ============================================================================ namespace { -// Material Design 3 Slider specifications (in dp) constexpr float TRACK_HEIGHT_DP = 8.0f; constexpr float THUMB_DIAMETER_DP = 20.0f; constexpr float TOUCH_TARGET_DP = 48.0f; @@ -59,75 +51,25 @@ constexpr float FOCUS_RING_MARGIN_DP = 4.0f; // Constructor / Destructor // ============================================================================ -Slider::Slider(QWidget* parent) : QSlider(Qt::Horizontal, parent) { - // Set size policy +Slider::Slider(QWidget* parent) + : QSlider(Qt::Horizontal, parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { if (orientation() == Qt::Horizontal) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } else { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); } - - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default cursor setCursor(Qt::PointingHandCursor); } -Slider::Slider(Qt::Orientation orientation, QWidget* parent) : QSlider(orientation, parent) { - // Set size policy +Slider::Slider(Qt::Orientation orientation, QWidget* parent) + : QSlider(orientation, parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { if (orientation == Qt::Horizontal) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } else { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); } - - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -141,80 +83,43 @@ Slider::~Slider() { void Slider::enterEvent(QEnterEvent* event) { QSlider::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Slider::leaveEvent(QEvent* event) { QSlider::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Slider::mousePressEvent(QMouseEvent* event) { QSlider::mousePressEvent(event); m_lastPressPos = event->pos(); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), thumbRect()); - if (m_elevation) - m_elevation->setPressed(true); - update(); + m_material.onMousePress(event->pos(), thumbRect()); } void Slider::mouseReleaseEvent(QMouseEvent* event) { QSlider::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation) - m_elevation->setPressed(false); - update(); + m_material.onMouseRelease(); } void Slider::mouseMoveEvent(QMouseEvent* event) { QSlider::mouseMoveEvent(event); - // Update ripple center if dragging - if (m_ripple && isSliderDown()) { - // Could update ripple position here for smooth follow effect - } } void Slider::focusInEvent(QFocusEvent* event) { QSlider::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Slider::focusOutEvent(QFocusEvent* event) { QSlider::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Slider::changeEvent(QEvent* event) { QSlider::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -224,22 +129,12 @@ void Slider::changeEvent(QEvent* event) { QSize Slider::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Material Design 3 slider specifications: - // - Track height: 4dp - // - Thumb diameter: 20dp - // - Touch target: 48dp minimum - float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); if (orientation() == Qt::Horizontal) { - int width = 200; // Minimum horizontal width - int height = int(std::ceil(touchTarget)); - return QSize(width, height); + return QSize(200, int(std::ceil(touchTarget))); } else { - int width = int(std::ceil(touchTarget)); - int height = 200; // Minimum vertical height - return QSize(width, height); + return QSize(int(std::ceil(touchTarget)), 200); } } @@ -248,13 +143,9 @@ QSize Slider::minimumSizeHint() const { float touchTarget = helper.dpToPx(TOUCH_TARGET_DP); if (orientation() == Qt::Horizontal) { - int width = 120; // Minimum horizontal width - int height = int(std::ceil(touchTarget)); - return QSize(width, height); + return QSize(120, int(std::ceil(touchTarget))); } else { - int width = int(std::ceil(touchTarget)); - int height = 120; // Minimum vertical height - return QSize(width, height); + return QSize(int(std::ceil(touchTarget)), 120); } } @@ -263,18 +154,17 @@ QSize Slider::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 + return CFColor(103, 80, 164); } inline CFColor fallbackSurfaceVariant() { - return CFColor(230, 225, 229); // Surface variant + return CFColor(230, 225, 229); } inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); // White + return CFColor(255, 255, 255); } inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); // Outline + return CFColor(120, 124, 132); } } // namespace @@ -336,7 +226,6 @@ float Slider::trackHeight() const { } float Slider::thumbRadius() const { - // Thumb is fully circular return thumbDiameter() / 2.0f; } @@ -358,29 +247,21 @@ float Slider::thumbPosition() const { return 0.0f; } - // Calculate position ratio (0.0 to 1.0) - float ratio = static_cast(curVal - minVal) / static_cast(maxVal - minVal); - return ratio; + return static_cast(curVal - minVal) / static_cast(maxVal - minVal); } QRectF Slider::trackRect() const { - CanvasUnitHelper helper(qApp->devicePixelRatio()); float tHeight = trackHeight(); float tDiameter = thumbDiameter(); - QRectF contentRect = rect(); if (orientation() == Qt::Horizontal) { - // Horizontal: center track vertically float y = (height() - tHeight) / 2.0f; - // Leave space for thumb at ends float thumbRadius = tDiameter / 2.0f; float x = thumbRadius; float w = width() - 2.0f * thumbRadius; return QRectF(x, y, w, tHeight); } else { - // Vertical: center track horizontally float x = (width() - tHeight) / 2.0f; - // Leave space for thumb at ends float thumbRadius = tDiameter / 2.0f; float y = thumbRadius; float h = height() - 2.0f * thumbRadius; @@ -393,16 +274,13 @@ QRectF Slider::activeTrackRect() const { float ratio = thumbPosition(); if (orientation() == Qt::Horizontal) { - // Active track should extend to thumb center, not thumb edge float thumbCenter = track.left() + track.width() * ratio; float activeWidth = thumbCenter - track.left(); return QRectF(track.left(), track.top(), activeWidth, track.height()); } else { - // Vertical: active part extends to thumb center from bottom float thumbCenter = track.bottom() - track.height() * ratio; float activeHeight = track.bottom() - thumbCenter; - float y = thumbCenter; - return QRectF(track.left(), y, track.width(), activeHeight); + return QRectF(track.left(), thumbCenter, track.width(), activeHeight); } } @@ -412,13 +290,11 @@ QRectF Slider::thumbRect() const { float tDiameter = thumbDiameter(); if (orientation() == Qt::Horizontal) { - // Calculate thumb center position float thumbCenter = track.left() + track.width() * ratio; float x = thumbCenter - tDiameter / 2.0f; float y = (height() - tDiameter) / 2.0f; return QRectF(x, y, tDiameter, tDiameter); } else { - // Vertical: calculate from bottom float thumbCenter = track.bottom() - track.height() * ratio; float x = (width() - tDiameter) / 2.0f; float y = thumbCenter - tDiameter / 2.0f; @@ -439,16 +315,14 @@ void Slider::paintEvent(QPaintEvent* event) { QRectF track = trackRect(); QRectF thumb = thumbRect(); - // Draw inactive track (only the portion after the thumb) + // Draw inactive track if (orientation() == Qt::Horizontal) { - // Right inactive track: 从滑块右边缘到轨道终点 qreal rightWidth = track.right() - thumb.right(); if (rightWidth > 0) { QRectF rightInactive(thumb.right(), track.top(), rightWidth, track.height()); drawInactiveTrack(p, rightInactive); } } else { - // Vertical: 类似逻辑,从下往上 qreal bottomHeight = track.bottom() - thumb.bottom(); if (bottomHeight > 0) { QRectF bottomInactive(track.left(), thumb.bottom(), track.width(), bottomHeight); @@ -462,13 +336,13 @@ void Slider::paintEvent(QPaintEvent* event) { } } - // Draw active track (延伸到滑块中心) + // Draw active track QRectF activeRect = activeTrackRect(); if (!activeRect.isEmpty()) { drawActiveTrack(p, activeRect); } - // Draw tick marks (if enabled) + // Draw tick marks if (tickPosition() != QSlider::NoTicks) { drawTickMarks(p, track); } @@ -491,7 +365,6 @@ void Slider::drawInactiveTrack(QPainter& p, const QRectF& rect) { CFColor tColor = inactiveTrackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } @@ -508,14 +381,12 @@ void Slider::drawActiveTrack(QPainter& p, const QRectF& rect) { CFColor tColor = activeTrackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); @@ -553,7 +424,6 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { int numTicks = (maxVal - minVal) / tickInterval; if (orientation() == Qt::Horizontal) { - // Draw ticks below the track float tickY = trackRect.bottom() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); for (int i = 0; i <= numTicks; ++i) { @@ -562,7 +432,6 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { p.drawLine(QPointF(x, tickY), QPointF(x, tickY + tickLength)); } } else { - // Draw ticks to the right of the track float tickX = trackRect.right() + helper.dpToPx(TICK_MARK_TO_TRACK_GAP_DP); for (int i = 0; i <= numTicks; ++i) { @@ -574,49 +443,40 @@ void Slider::drawTickMarks(QPainter& p, const QRectF& trackRect) { } void Slider::drawThumb(QPainter& p, const QRectF& rect) { - // Draw shadow first (for elevation effect) - if (m_elevation && isEnabled()) { + if (m_material.elevation() && isEnabled()) { QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_elevation->paintShadow(&p, thumbShape); + m_material.elevation()->paintShadow(&p, thumbShape); } CFColor tColor = thumbColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Draw thumb circle QPainterPath shape = roundedRect(rect, thumbRadius()); p.fillPath(shape, color); } void Slider::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, thumbRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } void Slider::drawFocusIndicator(QPainter& p, const QRectF& trackRect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(FOCUS_RING_MARGIN_DP); - QRectF focusRect; - if (orientation() == Qt::Horizontal) { - focusRect = trackRect.adjusted(-margin, -margin, margin, margin); - } else { - focusRect = trackRect.adjusted(-margin, -margin, margin, margin); - } + QRectF focusRect = trackRect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, trackHeight() / 2.0f + margin); - m_focusIndicator->paint(&p, shape, activeTrackColor()); + m_material.focusIndicator()->paint(&p, shape, activeTrackColor()); } } diff --git a/ui/widget/material/widget/slider/slider.h b/ui/widget/material/widget/slider/slider.h index 52eafef6e..e45e4dfe8 100644 --- a/ui/widget/material/widget/slider/slider.h +++ b/ui/widget/material/widget/slider/slider.h @@ -15,22 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -254,11 +245,7 @@ class CF_UI_EXPORT Slider : public QSlider { float thumbPosition() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Track last press position for ripple QPoint m_lastPressPos; diff --git a/ui/widget/material/widget/spinbox/spinbox.cpp b/ui/widget/material/widget/spinbox/spinbox.cpp index cf5774795..aff2865fe 100644 --- a/ui/widget/material/widget/spinbox/spinbox.cpp +++ b/ui/widget/material/widget/spinbox/spinbox.cpp @@ -16,11 +16,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,8 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -64,7 +58,14 @@ constexpr float kIconStrokeWidth = 2.5f; // Icon stroke width // ============================================================================ SpinBox::SpinBox(QWidget* parent) - : QSpinBox(parent), m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), + : QSpinBox(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = true, + .useFocusIndicator = true, + .initialElevation = 0, + }), + m_hoveringIncrementButton(false), m_hoveringDecrementButton(false), m_pressingIncrementButton(false), m_pressingDecrementButton(false) { // Disable native frame and background @@ -84,24 +85,6 @@ SpinBox::SpinBox(QWidget* parent) lineEdit()->setAlignment(Qt::AlignRight); } - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded for button areas - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Set default font setFont(textFont()); @@ -122,23 +105,16 @@ SpinBox::~SpinBox() { void SpinBox::enterEvent(QEnterEvent* event) { QSpinBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void SpinBox::leaveEvent(QEvent* event) { QSpinBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); + m_material.onLeaveEvent(); // Reset button hover state m_hoveringIncrementButton = false; m_hoveringDecrementButton = false; - - update(); } void SpinBox::mousePressEvent(QMouseEvent* event) { @@ -161,11 +137,7 @@ void SpinBox::mousePressEvent(QMouseEvent* event) { // Handle text area click QSpinBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(pos); - if (m_ripple) - m_ripple->onPress(pos, rect()); - update(); + m_material.onMousePress(pos, rect()); } void SpinBox::mouseReleaseEvent(QMouseEvent* event) { @@ -178,11 +150,7 @@ void SpinBox::mouseReleaseEvent(QMouseEvent* event) { } QSpinBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void SpinBox::mouseMoveEvent(QMouseEvent* event) { @@ -192,32 +160,18 @@ void SpinBox::mouseMoveEvent(QMouseEvent* event) { void SpinBox::focusInEvent(QFocusEvent* event) { QSpinBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void SpinBox::focusOutEvent(QFocusEvent* event) { QSpinBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void SpinBox::changeEvent(QEvent* event) { QSpinBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); updateTextColor(); update(); } @@ -546,11 +500,11 @@ void SpinBox::drawBackground(QPainter& p, const QPainterPath& shape) { } void SpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } - float opacity = m_stateMachine->stateLayerOpacity(); + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity <= 0.0f) { return; } @@ -563,10 +517,10 @@ void SpinBox::drawStateLayer(QPainter& p, const QPainterPath& shape) { } void SpinBox::drawRipple(QPainter& p, const QPainterPath& shape) { - if (m_ripple) { + if (m_material.ripple()) { // Set ripple color based on text color - m_ripple->setColor(textColor()); - m_ripple->paint(&p, shape); + m_material.ripple()->setColor(textColor()); + m_material.ripple()->paint(&p, shape); } } @@ -727,8 +681,8 @@ void SpinBox::drawButtons(QPainter& p, const QRectF& buttonRect) { } void SpinBox::drawFocusIndicator(QPainter& p, const QPainterPath& shape) { - if (m_focusIndicator && hasFocus()) { - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + if (m_material.focusIndicator() && hasFocus()) { + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } diff --git a/ui/widget/material/widget/spinbox/spinbox.h b/ui/widget/material/widget/spinbox/spinbox.h index 4069c7093..0ffabc0e4 100644 --- a/ui/widget/material/widget/spinbox/spinbox.h +++ b/ui/widget/material/widget/spinbox/spinbox.h @@ -18,21 +18,13 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -267,10 +259,7 @@ class CF_UI_EXPORT SpinBox : public QSpinBox { QFont textFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Internal state bool m_hoveringIncrementButton; diff --git a/ui/widget/material/widget/switch/switch.cpp b/ui/widget/material/widget/switch/switch.cpp index 7bda094af..fff18c516 100644 --- a/ui/widget/material/widget/switch/switch.cpp +++ b/ui/widget/material/widget/switch/switch.cpp @@ -15,14 +15,10 @@ #include "switch.h" #include "application_support/application.h" #include "base/device_pixel.h" +#include "base/easing.h" #include "base/geometry_helper.h" -#include "components/material/cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/elevation_controller.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -35,7 +31,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -46,11 +41,10 @@ using namespace cf::ui::widget::material::base; // ============================================================================ namespace { -// Material Design 3 Switch specifications (in dp) constexpr float TRACK_WIDTH_DP = 52.0f; constexpr float TRACK_HEIGHT_DP = 32.0f; constexpr float THUMB_DIAMETER_DP = 16.0f; -constexpr float THUMB_MARGIN_DP = 8.0f; // Distance from track edge +constexpr float THUMB_MARGIN_DP = 8.0f; constexpr float TEXT_SPACING_DP = 12.0f; constexpr float TOUCH_TARGET_DP = 48.0f; } // namespace @@ -59,41 +53,16 @@ constexpr float TOUCH_TARGET_DP = 48.0f; // Constructor / Destructor // ============================================================================ -Switch::Switch(QWidget* parent) : QCheckBox(parent) { - // Set size policy +Switch::Switch(QWidget* parent) + : QCheckBox(parent), + m_material(this, MaterialWidgetBase::Config{.useElevation = true, .initialElevation = 1}) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - // Get animation factory from Application - m_animationFactory = cf::WeakPtr::DynamicCast( - application_support::Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_elevation = new MdElevationController(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Set initial elevation for thumb (level 1) - m_elevation->setElevation(1); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - static_cast(&QWidget::update)); - - // Initialize thumb position based on current state m_thumbPosition = isChecked() ? 1.0f : 0.0f; if (isChecked()) { - m_stateMachine->onCheckedChanged(true); + m_material.stateMachine()->onCheckedChanged(true); } - // Set default cursor setCursor(Qt::PointingHandCursor); } @@ -111,11 +80,9 @@ void Switch::setChecked(bool checked) { } QCheckBox::setChecked(checked); - // When called from nextCheckState(), skip position update - animation - // will handle it. Otherwise (direct call), snap immediately. if (!m_inNextCheckState) { - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(checked); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(checked); } m_thumbPosition = checked ? 1.0f : 0.0f; update(); @@ -128,92 +95,54 @@ void Switch::setChecked(bool checked) { void Switch::enterEvent(QEnterEvent* event) { QCheckBox::enterEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverEnter(); - update(); + m_material.onEnterEvent(); } void Switch::leaveEvent(QEvent* event) { QCheckBox::leaveEvent(event); - if (m_stateMachine) - m_stateMachine->onHoverLeave(); - if (m_ripple) - m_ripple->onCancel(); - update(); + m_material.onLeaveEvent(); } void Switch::mousePressEvent(QMouseEvent* event) { QCheckBox::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), trackRect()); - if (m_elevation) - m_elevation->setPressed(true); - update(); + m_material.onMousePress(event->pos(), trackRect()); } void Switch::mouseReleaseEvent(QMouseEvent* event) { QCheckBox::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - if (m_elevation) - m_elevation->setPressed(false); - update(); + m_material.onMouseRelease(); } void Switch::focusInEvent(QFocusEvent* event) { QCheckBox::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); - update(); + m_material.onFocusIn(); } void Switch::focusOutEvent(QFocusEvent* event) { QCheckBox::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); - update(); + m_material.onFocusOut(); } void Switch::changeEvent(QEvent* event) { QCheckBox::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } void Switch::nextCheckState() { - // Set guard flag BEFORE calling base class - this prevents setChecked() - // from snapping the position, since we'll animate it ourselves m_inNextCheckState = true; QCheckBox::nextCheckState(); m_inNextCheckState = false; - // Update state machine checked state - if (m_stateMachine) { - m_stateMachine->onCheckedChanged(isChecked()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onCheckedChanged(isChecked()); } - // Animate thumb from current visual position to new target startThumbPositionAnimation(isChecked() ? 1.0f : 0.0f); } bool Switch::hitButton(const QPoint& pos) const { - // For custom-drawn switch, entire widget area is clickable return rect().contains(pos); } @@ -224,23 +153,15 @@ bool Switch::hitButton(const QPoint& pos) const { QSize Switch::sizeHint() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - // Material Design 3 switch specifications: - // - Track size: 52dp x 32dp - // - Spacing between track and text: 12dp - // - Touch target: 48dp minimum - float trackW = trackWidth(); - float trackH = trackHeight(); float spacing = helper.dpToPx(TEXT_SPACING_DP); float textWidth = text().isEmpty() ? 0.0f : fontMetrics().horizontalAdvance(text()); float width = trackW + spacing + textWidth; - - // Ensure minimum 48dp width for easy clicking (even without text) float minWidth = helper.dpToPx(TOUCH_TARGET_DP); width = std::max(width, minWidth); - float height = helper.dpToPx(TOUCH_TARGET_DP); // Fixed height for touch target + float height = helper.dpToPx(TOUCH_TARGET_DP); return QSize(int(std::ceil(width)), int(std::ceil(height))); } @@ -254,21 +175,20 @@ QSize Switch::minimumSizeHint() const { // ============================================================================ namespace { -// Fallback colors when theme is not available inline CFColor fallbackPrimary() { - return CFColor(103, 80, 164); // Purple 700 + return CFColor(103, 80, 164); } inline CFColor fallbackOutline() { - return CFColor(120, 124, 132); // Outline + return CFColor(120, 124, 132); } inline CFColor fallbackSurface() { - return CFColor(232, 226, 232); // Surface + return CFColor(232, 226, 232); } inline CFColor fallbackOnPrimary() { - return CFColor(255, 255, 255); // White + return CFColor(255, 255, 255); } inline CFColor fallbackOnSurface() { - return CFColor(29, 27, 32); // On Surface (near black) + return CFColor(29, 27, 32); } } // namespace @@ -282,7 +202,6 @@ CFColor Switch::trackColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Checked: PRIMARY, Unchecked: OUTLINE if (isChecked()) { return CFColor(colorScheme.queryColor(PRIMARY)); } @@ -302,7 +221,6 @@ CFColor Switch::thumbColor() const { const auto& theme = app->currentTheme(); auto& colorScheme = theme.color_scheme(); - // Checked: ON_PRIMARY (white), Unchecked: SURFACE if (isChecked()) { return CFColor(colorScheme.queryColor(ON_PRIMARY)); } @@ -347,12 +265,10 @@ CFColor Switch::labelColor() const { } float Switch::trackCornerRadius() const { - // Track uses full rounding (pill shape) return trackHeight() / 2.0f; } float Switch::thumbRadius() const { - // Thumb is fully circular return thumbDiameter() / 2.0f; } @@ -381,14 +297,9 @@ float Switch::thumbDiameter() const { QRectF Switch::trackRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); - - // Calculate vertical centering float trackH = trackHeight(); float y = (height() - trackH) / 2.0f; - - // Track starts from left float x = 0; - return QRectF(x, y, trackWidth(), trackH); } @@ -397,7 +308,6 @@ QRectF Switch::thumbRect() const { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(THUMB_MARGIN_DP); - // Calculate thumb position based on animation progress float maxTravel = track.width() - 2.0f * margin - thumbDiameter(); float currentX = track.left() + margin + maxTravel * m_thumbPosition; float y = track.top() + (track.height() - thumbDiameter()) / 2.0f; @@ -422,23 +332,20 @@ QRectF Switch::textRect() const { // ============================================================================ void Switch::startThumbPositionAnimation(float target) { - if (!m_animationFactory) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_thumbPosition = target; update(); return; } - // Get cached animation from factory - auto anim = m_animationFactory->createPropertyAnimation( - &m_thumbPosition, m_thumbPosition, target, 200, cf::ui::base::Easing::Type::Standard, this); + auto anim = factory->createPropertyAnimation(&m_thumbPosition, m_thumbPosition, target, 200, + cf::ui::base::Easing::Type::Standard, this); if (anim) { - // IMPORTANT: Update range to fix cached animation's stale from/to values - // The cached animation has old values from previous transition, which - // would cause the thumb to jump to wrong position (bounce bug). - if (auto* propAnim = - dynamic_cast( - anim.Get())) { + if (auto* propAnim = dynamic_cast(anim.Get())) { propAnim->setRange(m_thumbPosition, target); } anim->start(); @@ -486,20 +393,17 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { CFColor tColor = trackColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Handle state layer overlay - if (m_stateMachine && isEnabled()) { - float opacity = m_stateMachine->stateLayerOpacity(); + if (m_material.stateMachine() && isEnabled()) { + float opacity = m_material.stateMachine()->stateLayerOpacity(); if (opacity > 0.0f) { CFColor stateColor = stateLayerColor(); QColor stateQColor = stateColor.native_color(); stateQColor.setAlphaF(opacity); - // For unchecked state, blend with surface if (!isChecked()) { auto* app = application_support::Application::instance(); if (app) { @@ -514,7 +418,6 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { int(surface.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); color = QColor(r, g, b, color.alpha()); } catch (...) { - // Fallback to simple blend int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); @@ -528,7 +431,6 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { color = QColor(r, g, b, color.alpha()); } } else { - // Checked state: blend with primary int r = int(color.red() * (1.0f - opacity) + stateQColor.red() * opacity); int g = int(color.green() * (1.0f - opacity) + stateQColor.green() * opacity); int b = int(color.blue() * (1.0f - opacity) + stateQColor.blue() * opacity); @@ -542,35 +444,28 @@ void Switch::drawTrack(QPainter& p, const QRectF& rect) { } void Switch::drawThumb(QPainter& p, const QRectF& rect) { - // Draw shadow first (for elevation effect) - if (m_elevation && isEnabled()) { + if (m_material.elevation() && isEnabled()) { QPainterPath thumbShape = roundedRect(rect, thumbRadius()); - m_elevation->paintShadow(&p, thumbShape); + m_material.elevation()->paintShadow(&p, thumbShape); } CFColor tColor = thumbColor(); QColor color = tColor.native_color(); - // Handle disabled state if (!isEnabled()) { color.setAlphaF(0.38f); } - // Draw thumb circle QPainterPath shape = roundedRect(rect, thumbRadius()); p.fillPath(shape, color); - - // Optional: Draw check icon on thumb when checked - // (Material Design 3 switches often show an icon when checked) } void Switch::drawRipple(QPainter& p, const QRectF& rect) { - if (m_ripple) { - // Update ripple color based on current state - m_ripple->setColor(stateLayerColor()); + if (m_material.ripple()) { + m_material.ripple()->setColor(stateLayerColor()); QPainterPath clipPath = roundedRect(rect, trackCornerRadius()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); } } @@ -585,23 +480,20 @@ void Switch::drawText(QPainter& p, const QRectF& rect) { p.setPen(textColor.native_color()); } - // Use widget's font p.setFont(font()); - // Draw text vertically centered, left aligned - QRectF textBounds = rect.adjusted(0, 2, 0, -2); // Small adjustment for visual centering + QRectF textBounds = rect.adjusted(0, 2, 0, -2); p.drawText(textBounds, Qt::AlignLeft | Qt::AlignVCenter, text()); } void Switch::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { - // Expand rect slightly for focus ring + if (m_material.focusIndicator()) { CanvasUnitHelper helper(qApp->devicePixelRatio()); float margin = helper.dpToPx(4.0f); QRectF focusRect = rect.adjusted(-margin, -margin, margin, margin); QPainterPath shape = roundedRect(focusRect, trackCornerRadius() + margin); - m_focusIndicator->paint(&p, shape, trackColor()); + m_material.focusIndicator()->paint(&p, shape, trackColor()); } } diff --git a/ui/widget/material/widget/switch/switch.h b/ui/widget/material/widget/switch/switch.h index e924e9217..0f0f98382 100644 --- a/ui/widget/material/widget/switch/switch.h +++ b/ui/widget/material/widget/switch/switch.h @@ -15,22 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdElevationController; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -285,11 +276,7 @@ class CF_UI_EXPORT Switch : public QCheckBox { float thumbDiameter() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdElevationController* m_elevation; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Thumb position animation progress (0.0 = unchecked/left, 1.0 = checked/right) float m_thumbPosition = 0.0f; diff --git a/ui/widget/material/widget/tableview/tableview.cpp b/ui/widget/material/widget/tableview/tableview.cpp index de4fd18f6..a60f8acf2 100644 --- a/ui/widget/material/widget/tableview/tableview.cpp +++ b/ui/widget/material/widget/tableview/tableview.cpp @@ -15,11 +15,7 @@ #include "tableview.h" #include "application_support/application.h" #include "base/device_pixel.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -32,8 +28,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -67,27 +61,15 @@ inline CFColor fallbackPrimaryContainer() { // ============================================================================ TableView::TableView(QWidget* parent) - : QTableView(parent), rowHeight_(TableRowHeight::Standard), - gridStyle_(TableGridStyle::Horizontal), showHeader_(true), alternatingRowColors_(false), - rippleEnabled_(true), m_hasValidPressPos(false), m_hoveredRow(-1), m_pressedRow(-1) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode to bounded for table cells - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - + : QTableView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + rowHeight_(TableRowHeight::Standard), gridStyle_(TableGridStyle::Horizontal), + showHeader_(true), alternatingRowColors_(false), rippleEnabled_(true), + m_hasValidPressPos(false), m_hoveredRow(-1), m_pressedRow(-1) { // Configure default QTableView properties for Material rendering setAttribute(Qt::WA_Hover, true); setMouseTracking(true); @@ -186,8 +168,8 @@ bool TableView::rippleEnabled() const { void TableView::setRippleEnabled(bool enabled) { if (rippleEnabled_ != enabled) { rippleEnabled_ = enabled; - if (!enabled && m_ripple) { - m_ripple->onCancel(); + if (!enabled && m_material.ripple()) { + m_material.ripple()->onCancel(); } } } @@ -269,7 +251,7 @@ void TableView::paintEvent(QPaintEvent* event) { } // Draw ripple effects - if (rippleEnabled_ && m_ripple && (m_pressedRow >= 0 || m_hoveredRow >= 0)) { + if (rippleEnabled_ && m_material.ripple() && (m_pressedRow >= 0 || m_hoveredRow >= 0)) { int row = (m_pressedRow >= 0) ? m_pressedRow : m_hoveredRow; if (row >= 0) { QRect rowRect = visualRect(model()->index(row, 0)); @@ -277,8 +259,8 @@ void TableView::paintEvent(QPaintEvent* event) { rowRect.setWidth(viewportRect.width()); QPainterPath clipPath; clipPath.addRect(rowRect); - m_ripple->setColor(onSurfaceColor()); - m_ripple->paint(&p, clipPath); + m_material.ripple()->setColor(onSurfaceColor()); + m_material.ripple()->paint(&p, clipPath); } } @@ -288,7 +270,7 @@ void TableView::paintEvent(QPaintEvent* event) { } // Draw focus indicator around the actual content area - if (hasFocus() && m_focusIndicator && model()) { + if (hasFocus() && m_material.focusIndicator() && model()) { // Calculate actual content bounds QRectF contentRect = viewportRect; @@ -326,15 +308,15 @@ void TableView::mousePressEvent(QMouseEvent* event) { int row = index.row(); if (row >= 0) { m_pressedRow = row; - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } - if (m_ripple && rippleEnabled_) { + if (m_material.ripple() && rippleEnabled_) { QRect rowRect = visualRect(model()->index(row, 0)); rowRect.setLeft(0); rowRect.setWidth(viewport()->width()); // Ripple expects viewport coordinates since rowRect is in viewport coords - m_ripple->onPress(viewportPos, rowRect); + m_material.ripple()->onPress(viewportPos, rowRect); } update(); } @@ -345,11 +327,11 @@ void TableView::mouseReleaseEvent(QMouseEvent* event) { QTableView::mouseReleaseEvent(event); if (m_pressedRow >= 0) { - if (m_stateMachine) { - m_stateMachine->onRelease(); + if (m_material.stateMachine()) { + m_material.stateMachine()->onRelease(); } - if (m_ripple && rippleEnabled_) { - m_ripple->onRelease(); + if (m_material.ripple() && rippleEnabled_) { + m_material.ripple()->onRelease(); } m_pressedRow = -1; m_hasValidPressPos = false; @@ -368,8 +350,8 @@ void TableView::mouseMoveEvent(QMouseEvent* event) { if (m_hoveredRow != row) { m_hoveredRow = row; - if (row >= 0 && m_stateMachine) { - m_stateMachine->onHoverEnter(); + if (row >= 0 && m_material.stateMachine()) { + m_material.stateMachine()->onHoverEnter(); } update(); } @@ -377,20 +359,12 @@ void TableView::mouseMoveEvent(QMouseEvent* event) { void TableView::enterEvent(QEnterEvent* event) { QTableView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } - update(); + m_material.onEnterEvent(); } void TableView::leaveEvent(QEvent* event) { QTableView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); m_hoveredRow = -1; m_pressedRow = -1; update(); @@ -398,37 +372,18 @@ void TableView::leaveEvent(QEvent* event) { void TableView::focusInEvent(QFocusEvent* event) { QTableView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void TableView::focusOutEvent(QFocusEvent* event) { QTableView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void TableView::changeEvent(QEvent* event) { QTableView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -487,7 +442,7 @@ void TableView::drawGridLines(QPainter& p, const QRectF& viewportRect) { } void TableView::drawFocusIndicator(QPainter& p, const QRectF& rect) { - if (m_focusIndicator) { + if (m_material.focusIndicator()) { // Create focus indicator path (inset to account for both ring width and spacing) // Ring width: 3dp, Additional inset: 3dp = Total 6dp QPainterPath path; @@ -496,7 +451,7 @@ void TableView::drawFocusIndicator(QPainter& p, const QRectF& rect) { QRectF focusRect = rect.adjusted(-inset, -inset, inset, inset); path.addRoundedRect(focusRect, 4, 4); - m_focusIndicator->paint(&p, path, primaryContainerColor()); + m_material.focusIndicator()->paint(&p, path, primaryContainerColor()); } } diff --git a/ui/widget/material/widget/tableview/tableview.h b/ui/widget/material/widget/tableview/tableview.h index 7a99b5b86..a8e2e902e 100644 --- a/ui/widget/material/widget/tableview/tableview.h +++ b/ui/widget/material/widget/tableview/tableview.h @@ -15,21 +15,13 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -401,10 +393,7 @@ class CF_UI_EXPORT TableView : public QTableView { CFColor primaryContainerColor() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Properties TableRowHeight rowHeight_; diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.cpp b/ui/widget/material/widget/tabview/private/materialtabbar.cpp index 95e233bae..adaa9e08d 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.cpp +++ b/ui/widget/material/widget/tabview/private/materialtabbar.cpp @@ -14,8 +14,6 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/state_machine.h" #include #include @@ -27,7 +25,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -58,7 +55,11 @@ MaterialTabBar::MaterialTabBar(TabView* parent) : QTabBar(parent), m_tabView(parent), m_tabHeightDp(DEFAULT_TAB_HEIGHT_DP), m_tabMinWidthDp(DEFAULT_TAB_MIN_WIDTH_DP), m_showIndicator(true), m_indicatorPosition(0.0f), m_indicatorTargetPosition(0.0f), m_lastIndex(-1), m_hoveredIndex(-1), m_pressedIndex(-1), - m_closeButtonHoveredIndex(-1), m_stateMachine(nullptr), m_focusIndicator(nullptr) { + m_closeButtonHoveredIndex(-1), m_material(this, base::MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }) { setDrawBase(false); setDocumentMode(true); @@ -67,14 +68,6 @@ MaterialTabBar::MaterialTabBar(TabView* parent) setElideMode(Qt::ElideRight); setMouseTracking(true); - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - m_stateMachine = new StateMachine(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); connect(this, &QTabBar::currentChanged, this, &MaterialTabBar::animateIndicatorTo); } @@ -153,15 +146,15 @@ void MaterialTabBar::drawTab(QPainter& p, int index) { drawTabStateLayer(p, tabRect, index); drawTabContent(p, tabRect, index); - if (index == currentIndex() && hasFocus() && m_focusIndicator) { + if (index == currentIndex() && hasFocus() && m_material.focusIndicator()) { QPainterPath shape; shape.addRoundedRect(tabRect.adjusted(2, 2, -2, -2), 4, 4); - m_focusIndicator->paint(&p, shape, focusIndicatorColor()); + m_material.focusIndicator()->paint(&p, shape, focusIndicatorColor()); } } void MaterialTabBar::drawTabStateLayer(QPainter& p, const QRect& tabRect, int index) { - if (!isEnabled() || !m_stateMachine) { + if (!isEnabled() || !m_material.stateMachine()) { return; } @@ -263,7 +256,9 @@ void MaterialTabBar::animateIndicatorTo(int index) { int oldIndex = m_lastIndex; m_lastIndex = index; - if (!m_animationFactory || !m_animationFactory->isAllEnabled()) { + auto factory = + cf::WeakPtr::DynamicCast(Application::animationFactory()); + if (!factory || !factory->isAllEnabled()) { m_indicatorPosition = static_cast(tabRect(index).left()); update(); return; @@ -355,17 +350,13 @@ void MaterialTabBar::drawCloseIcon(QPainter& p, const QRect& closeRect, int inde void MaterialTabBar::enterEvent(QEnterEvent* event) { QTabBar::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } + m_material.onEnterEvent(); update(); } void MaterialTabBar::leaveEvent(QEvent* event) { QTabBar::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } + m_material.onLeaveEvent(); m_hoveredIndex = -1; m_closeButtonHoveredIndex = -1; update(); @@ -388,9 +379,7 @@ void MaterialTabBar::mousePressEvent(QMouseEvent* event) { int index = tabAt(event->pos()); if (index >= 0) { m_pressedIndex = index; - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); - } + m_material.onMousePress(event->pos(), QRectF(tabRect(index))); } QTabBar::mousePressEvent(event); update(); @@ -398,9 +387,7 @@ void MaterialTabBar::mousePressEvent(QMouseEvent* event) { void MaterialTabBar::mouseReleaseEvent(QMouseEvent* event) { m_pressedIndex = -1; - if (m_stateMachine) { - m_stateMachine->onRelease(); - } + m_material.onMouseRelease(); // Check if close button was clicked int index = tabAt(event->pos()); @@ -416,32 +403,20 @@ void MaterialTabBar::mouseReleaseEvent(QMouseEvent* event) { void MaterialTabBar::focusInEvent(QFocusEvent* event) { QTabBar::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } + m_material.onFocusIn(); update(); } void MaterialTabBar::focusOutEvent(QFocusEvent* event) { QTabBar::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } + m_material.onFocusOut(); update(); } void MaterialTabBar::changeEvent(QEvent* event) { QTabBar::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - isEnabled() ? m_stateMachine->onEnable() : m_stateMachine->onDisable(); - } + m_material.onEnabledChange(isEnabled()); update(); } } diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.h b/ui/widget/material/widget/tabview/private/materialtabbar.h index 81d949987..254d1012e 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.h +++ b/ui/widget/material/widget/tabview/private/materialtabbar.h @@ -21,20 +21,14 @@ #include "base/device_pixel.h" #include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "material/base/elevation_controller.h" #include "material/widget/button/button.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { // Forward declarations class TabView; -namespace base { -class StateMachine; -class MdFocusIndicator; -} // namespace base -using namespace cf::ui::components::material; using CFColor = base::CFColor; /** * @brief Internal Material TabBar implementation. @@ -389,10 +383,8 @@ class MaterialTabBar : public QTabBar { QSet m_closeableTabs; // Set of closeable tab indices - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::MdFocusIndicator* m_focusIndicator; cf::WeakPtr m_indicatorAnimation; + base::MaterialWidgetBase m_material; }; } // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/textarea/textarea.cpp b/ui/widget/material/widget/textarea/textarea.cpp index cb4026e24..05feff4e6 100644 --- a/ui/widget/material/widget/textarea/textarea.cpp +++ b/ui/widget/material/widget/textarea/textarea.cpp @@ -17,11 +17,7 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -75,9 +71,15 @@ inline CFColor fallbackPrimary() { // ============================================================================ TextArea::TextArea(TextAreaVariant variant, QWidget* parent) - : QTextEdit(parent), m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), - m_minLines(1), m_maxLines(0), m_isFloating(false), m_hasError(false), - m_floatingProgress(0.0f), m_updatingGeometry(false) { + : QTextEdit(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_minLines(1), + m_maxLines(0), m_isFloating(false), m_hasError(false), m_floatingProgress(0.0f), + m_updatingGeometry(false) { // Disable native frame setFrameStyle(QFrame::NoFrame); @@ -86,24 +88,6 @@ TextArea::TextArea(TextAreaVariant variant, QWidget* parent) setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Connect text change signal connect(this, &QTextEdit::textChanged, this, &TextArea::textChanged); @@ -141,53 +125,30 @@ TextArea::~TextArea() { void TextArea::mousePressEvent(QMouseEvent* event) { QTextEdit::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); - update(); + m_material.onMousePress(event->pos(), rect()); } void TextArea::mouseReleaseEvent(QMouseEvent* event) { QTextEdit::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); - update(); + m_material.onMouseRelease(); } void TextArea::focusInEvent(QFocusEvent* event) { QTextEdit::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); + m_material.onFocusIn(); updateFloatingState(true); - update(); } void TextArea::focusOutEvent(QFocusEvent* event) { QTextEdit::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); + m_material.onFocusOut(); updateFloatingState(!toPlainText().isEmpty()); - update(); } void TextArea::changeEvent(QEvent* event) { QTextEdit::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -815,16 +776,16 @@ void TextArea::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { void TextArea::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { if (!p.isActive()) return; // guard - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) { if (!p.isActive()) return; // guard - if (m_ripple) { + if (m_material.ripple()) { QPainterPath shape; if (m_variant == TextAreaVariant::Outlined) { shape = roundedRect(fieldRect, cornerRadius()); @@ -832,7 +793,7 @@ void TextArea::drawRipple(QPainter& p, const QRectF& fieldRect) { // For filled variant, clip to background shape.addRect(fieldRect); } - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -856,16 +817,19 @@ void TextArea::animateFloatingTo(bool floating) { return; } - if (!m_animationFactory) { + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_floatingProgress = target; update(); return; } // Create property animation for floating progress (same approach as TextField) - auto anim = m_animationFactory->createPropertyAnimation( - &m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { anim->start(); diff --git a/ui/widget/material/widget/textarea/textarea.h b/ui/widget/material/widget/textarea/textarea.h index 8a4ce266a..2f7f38981 100644 --- a/ui/widget/material/widget/textarea/textarea.h +++ b/ui/widget/material/widget/textarea/textarea.h @@ -21,19 +21,11 @@ #include #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - using CFColor = cf::ui::base::CFColor; /** @@ -519,10 +511,7 @@ class CF_UI_EXPORT TextArea : public QTextEdit { QFont helperFont() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Properties TextAreaVariant m_variant; diff --git a/ui/widget/material/widget/textfield/textfield.cpp b/ui/widget/material/widget/textfield/textfield.cpp index 740852f8b..4c4d6ad41 100644 --- a/ui/widget/material/widget/textfield/textfield.cpp +++ b/ui/widget/material/widget/textfield/textfield.cpp @@ -17,12 +17,10 @@ #include "application_support/application.h" #include "base/device_pixel.h" #include "base/geometry_helper.h" +#include "base/include/base/weak_ptr/weak_ptr.h" #include "cfmaterial_animation_factory.h" #include "components/material/cfmaterial_property_animation.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -36,7 +34,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; using namespace cf::ui::base::geometry; -using namespace cf::ui::components; using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; @@ -74,8 +71,14 @@ inline CFColor fallbackPrimary() { // ============================================================================ TextField::TextField(TextFieldVariant variant, QWidget* parent) - : QLineEdit(parent), m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), - m_isFloating(false), m_hasError(false), m_floatingProgress(0.0f), m_outlineWidth(1.0f), + : QLineEdit(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_variant(variant), m_showCharacterCounter(false), m_maxLength(0), m_isFloating(false), + m_hasError(false), m_floatingProgress(0.0f), m_outlineWidth(1.0f), m_hoveringClearButton(false), m_pressingClearButton(false) { // Disable native frame and background @@ -83,24 +86,6 @@ TextField::TextField(TextFieldVariant variant, QWidget* parent) setAttribute(Qt::WA_TranslucentBackground); setTextMargins(0, 0, 0, 0); - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - - // Set ripple mode - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Connect text change signal connect(this, &QLineEdit::textChanged, this, &TextField::textChanged); @@ -134,10 +119,10 @@ void TextField::mousePressEvent(QMouseEvent* event) { } QLineEdit::mousePressEvent(event); - if (m_stateMachine) - m_stateMachine->onPress(event->pos()); - if (m_ripple) - m_ripple->onPress(event->pos(), rect()); + if (m_material.stateMachine()) + m_material.stateMachine()->onPress(event->pos()); + if (m_material.ripple()) + m_material.ripple()->onPress(event->pos(), rect()); update(); } @@ -154,29 +139,29 @@ void TextField::mouseReleaseEvent(QMouseEvent* event) { } QLineEdit::mouseReleaseEvent(event); - if (m_stateMachine) - m_stateMachine->onRelease(); - if (m_ripple) - m_ripple->onRelease(); + if (m_material.stateMachine()) + m_material.stateMachine()->onRelease(); + if (m_material.ripple()) + m_material.ripple()->onRelease(); update(); } void TextField::focusInEvent(QFocusEvent* event) { QLineEdit::focusInEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusIn(); - if (m_focusIndicator) - m_focusIndicator->onFocusIn(); + if (m_material.stateMachine()) + m_material.stateMachine()->onFocusIn(); + if (m_material.focusIndicator()) + m_material.focusIndicator()->onFocusIn(); updateFloatingState(true); update(); } void TextField::focusOutEvent(QFocusEvent* event) { QLineEdit::focusOutEvent(event); - if (m_stateMachine) - m_stateMachine->onFocusOut(); - if (m_focusIndicator) - m_focusIndicator->onFocusOut(); + if (m_material.stateMachine()) + m_material.stateMachine()->onFocusOut(); + if (m_material.focusIndicator()) + m_material.focusIndicator()->onFocusOut(); updateFloatingState(!text().isEmpty()); update(); } @@ -184,13 +169,7 @@ void TextField::focusOutEvent(QFocusEvent* event) { void TextField::changeEvent(QEvent* event) { QLineEdit::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } + m_material.onEnabledChange(isEnabled()); update(); } } @@ -1035,14 +1014,14 @@ void TextField::drawCharacterCounter(QPainter& p, const QRectF& helperRect) { } void TextField::drawFocusIndicator(QPainter& p, const QRectF& fieldRect) { - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QPainterPath shape = roundedRect(fieldRect, cornerRadius()); - m_focusIndicator->paint(&p, shape, focusOutlineColor()); + m_material.focusIndicator()->paint(&p, shape, focusOutlineColor()); } } void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) { - if (m_ripple) { + if (m_material.ripple()) { QPainterPath shape; if (m_variant == TextFieldVariant::Outlined) { shape = roundedRect(fieldRect, cornerRadius()); @@ -1050,7 +1029,7 @@ void TextField::drawRipple(QPainter& p, const QRectF& fieldRect) { // For filled variant, clip to background shape.addRect(fieldRect); } - m_ripple->paint(&p, shape); + m_material.ripple()->paint(&p, shape); } } @@ -1074,16 +1053,20 @@ void TextField::animateFloatingTo(bool floating) { return; } - if (!m_animationFactory) { + // Get animation factory locally for custom property animations + auto factory = cf::WeakPtr::DynamicCast( + application_support::Application::animationFactory()); + + if (!factory) { m_floatingProgress = target; update(); return; } // Create property animation for floating progress - auto anim = m_animationFactory->createPropertyAnimation( - &m_floatingProgress, m_floatingProgress, target, 200, - cf::ui::base::Easing::Type::EmphasizedDecelerate, this); + auto anim = + factory->createPropertyAnimation(&m_floatingProgress, m_floatingProgress, target, 200, + cf::ui::base::Easing::Type::EmphasizedDecelerate, this); if (anim) { // IMPORTANT: Update range to fix cached animation's stale from/to values diff --git a/ui/widget/material/widget/textfield/textfield.h b/ui/widget/material/widget/textfield/textfield.h index 2c0ff7858..92e08f538 100644 --- a/ui/widget/material/widget/textfield/textfield.h +++ b/ui/widget/material/widget/textfield/textfield.h @@ -1,540 +1,528 @@ -/** - * @file ui/widget/material/widget/textfield/textfield.h - * @brief Material Design 3 TextField widget. - * - * Implements Material Design 3 text field with support for filled and outlined - * variants. Includes floating labels, prefix/suffix icons, character counter, - * helper/error text, and password mode. - * - * @author CFDesktop Team - * @date 2026-03-01 - * @version 0.1 - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -#pragma once - -#include -#include -#include - -#include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" -#include "export.h" - -namespace cf::ui::widget::material { - -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - -using CFColor = cf::ui::base::CFColor; - -/** - * @brief Material Design 3 TextField widget. - * - * @details Implements Material Design 3 text field with support for filled - * and outlined variants. Includes floating labels, prefix/suffix icons, - * character counter, helper/error text, and password mode. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ -class CF_UI_EXPORT TextField : public QLineEdit { - Q_OBJECT - Q_PROPERTY(TextFieldVariant variant READ variant WRITE setVariant) - Q_PROPERTY(QString label READ label WRITE setLabel) - Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) - Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) - Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) - Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE - setShowCharacterCounter) - Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) - Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) - Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) - - public: - /** - * @brief TextField visual variant. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - enum class TextFieldVariant { Filled, Outlined }; - Q_ENUM(TextFieldVariant); - - /** - * @brief Constructor with variant. - * - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Constructor with text and variant. - * - * @param[in] text Initial text content. - * @param[in] variant TextField visual variant. - * @param[in] parent QObject parent. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - explicit TextField(const QString& text, TextFieldVariant variant = TextFieldVariant::Filled, - QWidget* parent = nullptr); - - /** - * @brief Destructor. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - ~TextField() override; - - /** - * @brief Gets the text field variant. - * - * @return Current text field variant. - * - * @throws None - * @note None - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - TextFieldVariant variant() const; - - /** - * @brief Sets the text field variant. - * - * @param[in] variant TextField variant to use. - * - * @throws None - * @note Changing variant updates the visual appearance. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setVariant(TextFieldVariant variant); - - /** - * @brief Gets the label text. - * - * @return Current label text. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString label() const; - - /** - * @brief Sets the label text. - * - * @param[in] label Label text to display. - * - * @throws None - * @note The label floats when the field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setLabel(const QString& label); - - /** - * @brief Gets the helper text. - * - * @return Current helper text. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString helperText() const; - - /** - * @brief Sets the helper text. - * - * @param[in] text Helper text to display. - * - * @throws None - * @note Helper text is displayed below the text field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setHelperText(const QString& text); - - /** - * @brief Gets the error text. - * - * @return Current error text. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QString errorText() const; - - /** - * @brief Sets the error text. - * - * @param[in] text Error text to display. Empty string clears error state. - * - * @throws None - * @note Error text takes precedence over helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setErrorText(const QString& text); - - /** - * @brief Checks if the label is in floating state. - * - * @return true if label is floating, false otherwise. - * - * @throws None - * @note Label floats when field has focus or content. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool isFloating() const; - - /** - * @brief Gets the prefix icon. - * - * @return Current prefix icon. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon prefixIcon() const; - - /** - * @brief Sets the prefix icon. - * - * @param[in] icon Icon to display at the start of the field. - * - * @throws None - * @note Prefix icon is displayed at the start of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setPrefixIcon(const QIcon& icon); - - /** - * @brief Gets the suffix icon. - * - * @return Current suffix icon. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QIcon suffixIcon() const; - - /** - * @brief Sets the suffix icon. - * - * @param[in] icon Icon to display at the end of the field. - * - * @throws None - * @note Suffix icon is displayed at the end of the field. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setSuffixIcon(const QIcon& icon); - - /** - * @brief Gets whether character counter is shown. - * - * @return true if character counter is visible, false otherwise. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - bool showCharacterCounter() const; - - /** - * @brief Sets whether character counter is shown. - * - * @param[in] show true to show character counter, false to hide. - * - * @throws None - * @note Character counter is shown in the helper text area. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setShowCharacterCounter(bool show); - - /** - * @brief Gets the maximum length. - * - * @return Maximum character length. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - int maxLength() const; - - /** - * @brief Sets the maximum length. - * - * @param[in] length Maximum character length. 0 means no maximum. - * - * @throws None - * @note 0 means no maximum. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void setMaxLength(int length); - - /** - * @brief Gets the recommended size. - * - * @return Recommended size for the text field. - * - * @throws None - * @note Based on label, icons, padding, and helper text. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize sizeHint() const override; - - /** - * @brief Gets the minimum recommended size. - * - * @return Minimum recommended size. - * - * @throws None - * @note Ensures touch target size requirements. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - QSize minimumSizeHint() const override; - - signals: - /** - * @brief Signal emitted when floating state changes. - * - * @param[in] floating true if label is floating, false otherwise. - * - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void floatingChanged(bool floating); - - protected: - /** - * @brief Paints the text field. - * - * @param[in] event Paint event. - * - * @throws None - * @note Implements Material Design paint pipeline. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void paintEvent(QPaintEvent* event) override; - - /** - * @brief Handles resize event. - * - * @param[in] event Resize event. - * - * @throws None - * @note Updates internal layout calculations. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void resizeEvent(QResizeEvent* event) override; - - /** - * @brief Handles mouse press event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Triggers ripple effect. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mousePressEvent(QMouseEvent* event) override; - - /** - * @brief Handles mouse release event. - * - * @param[in] event Mouse event. - * - * @throws None - * @note Updates ripple state. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void mouseReleaseEvent(QMouseEvent* event) override; - - /** - * @brief Handles focus in event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and shows focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusInEvent(QFocusEvent* event) override; - - /** - * @brief Handles focus out event. - * - * @param[in] event Focus event. - * - * @throws None - * @note Updates floating label state and hides focus indicator. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void focusOutEvent(QFocusEvent* event) override; - - /** - * @brief Handles widget state change event. - * - * @param[in] event Change event. - * - * @throws None - * @note Updates appearance based on state changes. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void changeEvent(QEvent* event) override; - - /** - * @brief Handles text change event. - * - * @param[in] text New text content. - * - * @throws None - * @note Updates floating label state and character counter. - * @warning None - * @since 0.1 - * @ingroup ui_widget_material_widget - */ - void textChanged(const QString& text); - - private: - // Drawing helpers - Material Design paint pipeline - void drawBackground(QPainter& p, const QRectF& fieldRect); - void drawOutline(QPainter& p, const QRectF& fieldRect); - void drawLabel(QPainter& p, const QRectF& fieldRect); - void drawText(QPainter& p, const QRectF& textRect); - void drawPrefixIcon(QPainter& p, const QRectF& textRect); - void drawSuffixIcon(QPainter& p, const QRectF& textRect); - void drawClearButton(QPainter& p, const QRectF& textRect); - void drawHelperText(QPainter& p, const QRectF& helperRect); - void drawCharacterCounter(QPainter& p, const QRectF& helperRect); - void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); - void drawRipple(QPainter& p, const QRectF& fieldRect); - - // Layout helpers - QRectF fieldRect() const; - QRectF textRect() const; - QRectF helperTextRect() const; - QRectF prefixIconRect() const; - QRectF suffixIconRect() const; - QRectF clearButtonRect() const; - - // Animation helpers - void updateFloatingState(bool shouldFloat); - void animateFloatingTo(bool floating); - - // Color access methods - CFColor containerColor() const; - CFColor onContainerColor() const; - CFColor labelColor() const; - CFColor inputTextColor() const; - CFColor outlineColor() const; - CFColor focusOutlineColor() const; - CFColor errorColor() const; - CFColor helperTextColor() const; - float cornerRadius() const; - QFont inputFont() const; - QFont labelFont() const; - QFont helperFont() const; - - // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; - - // Properties - TextFieldVariant m_variant; - QString m_label; - QString m_helperText; - QString m_errorText; - QIcon m_prefixIcon; - QIcon m_suffixIcon; - bool m_showCharacterCounter; - int m_maxLength; - - // Internal state - bool m_isFloating; - bool m_hasError; - float m_floatingProgress; // 0.0 = resting, 1.0 = floating - float m_outlineWidth; // For animating outline width - bool m_hoveringClearButton; - bool m_pressingClearButton; -}; - -} // namespace cf::ui::widget::material +/** + * @file ui/widget/material/widget/textfield/textfield.h + * @brief Material Design 3 TextField widget. + * + * Implements Material Design 3 text field with support for filled and outlined + * variants. Includes floating labels, prefix/suffix icons, character counter, + * helper/error text, and password mode. + * + * @author CFDesktop Team + * @date 2026-03-01 + * @version 0.1 + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +#pragma once + +#include +#include +#include + +#include "base/color.h" +#include "export.h" +#include "widget/material/base/material_widget_base.h" + +namespace cf::ui::widget::material { + +using CFColor = cf::ui::base::CFColor; + +/** + * @brief Material Design 3 TextField widget. + * + * @details Implements Material Design 3 text field with support for filled + * and outlined variants. Includes floating labels, prefix/suffix icons, + * character counter, helper/error text, and password mode. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ +class CF_UI_EXPORT TextField : public QLineEdit { + Q_OBJECT + Q_PROPERTY(TextFieldVariant variant READ variant WRITE setVariant) + Q_PROPERTY(QString label READ label WRITE setLabel) + Q_PROPERTY(QString helperText READ helperText WRITE setHelperText) + Q_PROPERTY(QString errorText READ errorText WRITE setErrorText) + Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) + Q_PROPERTY(bool showCharacterCounter READ showCharacterCounter WRITE setShowCharacterCounter) + Q_PROPERTY(bool isFloating READ isFloating NOTIFY floatingChanged) + Q_PROPERTY(QIcon prefixIcon READ prefixIcon WRITE setPrefixIcon) + Q_PROPERTY(QIcon suffixIcon READ suffixIcon WRITE setSuffixIcon) + + public: + /** + * @brief TextField visual variant. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + enum class TextFieldVariant { Filled, Outlined }; + Q_ENUM(TextFieldVariant); + + /** + * @brief Constructor with variant. + * + * @param[in] variant TextField visual variant. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit TextField(TextFieldVariant variant = TextFieldVariant::Filled, + QWidget* parent = nullptr); + + /** + * @brief Constructor with text and variant. + * + * @param[in] text Initial text content. + * @param[in] variant TextField visual variant. + * @param[in] parent QObject parent. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + explicit TextField(const QString& text, TextFieldVariant variant = TextFieldVariant::Filled, + QWidget* parent = nullptr); + + /** + * @brief Destructor. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + ~TextField() override; + + /** + * @brief Gets the text field variant. + * + * @return Current text field variant. + * + * @throws None + * @note None + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + TextFieldVariant variant() const; + + /** + * @brief Sets the text field variant. + * + * @param[in] variant TextField variant to use. + * + * @throws None + * @note Changing variant updates the visual appearance. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setVariant(TextFieldVariant variant); + + /** + * @brief Gets the label text. + * + * @return Current label text. + * + * @throws None + * @note The label floats when the field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString label() const; + + /** + * @brief Sets the label text. + * + * @param[in] label Label text to display. + * + * @throws None + * @note The label floats when the field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setLabel(const QString& label); + + /** + * @brief Gets the helper text. + * + * @return Current helper text. + * + * @throws None + * @note Helper text is displayed below the text field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString helperText() const; + + /** + * @brief Sets the helper text. + * + * @param[in] text Helper text to display. + * + * @throws None + * @note Helper text is displayed below the text field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setHelperText(const QString& text); + + /** + * @brief Gets the error text. + * + * @return Current error text. + * + * @throws None + * @note Error text takes precedence over helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QString errorText() const; + + /** + * @brief Sets the error text. + * + * @param[in] text Error text to display. Empty string clears error state. + * + * @throws None + * @note Error text takes precedence over helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setErrorText(const QString& text); + + /** + * @brief Checks if the label is in floating state. + * + * @return true if label is floating, false otherwise. + * + * @throws None + * @note Label floats when field has focus or content. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool isFloating() const; + + /** + * @brief Gets the prefix icon. + * + * @return Current prefix icon. + * + * @throws None + * @note Prefix icon is displayed at the start of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QIcon prefixIcon() const; + + /** + * @brief Sets the prefix icon. + * + * @param[in] icon Icon to display at the start of the field. + * + * @throws None + * @note Prefix icon is displayed at the start of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setPrefixIcon(const QIcon& icon); + + /** + * @brief Gets the suffix icon. + * + * @return Current suffix icon. + * + * @throws None + * @note Suffix icon is displayed at the end of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QIcon suffixIcon() const; + + /** + * @brief Sets the suffix icon. + * + * @param[in] icon Icon to display at the end of the field. + * + * @throws None + * @note Suffix icon is displayed at the end of the field. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setSuffixIcon(const QIcon& icon); + + /** + * @brief Gets whether character counter is shown. + * + * @return true if character counter is visible, false otherwise. + * + * @throws None + * @note Character counter is shown in the helper text area. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + bool showCharacterCounter() const; + + /** + * @brief Sets whether character counter is shown. + * + * @param[in] show true to show character counter, false to hide. + * + * @throws None + * @note Character counter is shown in the helper text area. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setShowCharacterCounter(bool show); + + /** + * @brief Gets the maximum length. + * + * @return Maximum character length. + * + * @throws None + * @note 0 means no maximum. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + int maxLength() const; + + /** + * @brief Sets the maximum length. + * + * @param[in] length Maximum character length. 0 means no maximum. + * + * @throws None + * @note 0 means no maximum. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void setMaxLength(int length); + + /** + * @brief Gets the recommended size. + * + * @return Recommended size for the text field. + * + * @throws None + * @note Based on label, icons, padding, and helper text. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize sizeHint() const override; + + /** + * @brief Gets the minimum recommended size. + * + * @return Minimum recommended size. + * + * @throws None + * @note Ensures touch target size requirements. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + QSize minimumSizeHint() const override; + + signals: + /** + * @brief Signal emitted when floating state changes. + * + * @param[in] floating true if label is floating, false otherwise. + * + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void floatingChanged(bool floating); + + protected: + /** + * @brief Paints the text field. + * + * @param[in] event Paint event. + * + * @throws None + * @note Implements Material Design paint pipeline. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Handles resize event. + * + * @param[in] event Resize event. + * + * @throws None + * @note Updates internal layout calculations. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void resizeEvent(QResizeEvent* event) override; + + /** + * @brief Handles mouse press event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Triggers ripple effect. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mousePressEvent(QMouseEvent* event) override; + + /** + * @brief Handles mouse release event. + * + * @param[in] event Mouse event. + * + * @throws None + * @note Updates ripple state. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + /** + * @brief Handles focus in event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Updates floating label state and shows focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusInEvent(QFocusEvent* event) override; + + /** + * @brief Handles focus out event. + * + * @param[in] event Focus event. + * + * @throws None + * @note Updates floating label state and hides focus indicator. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void focusOutEvent(QFocusEvent* event) override; + + /** + * @brief Handles widget state change event. + * + * @param[in] event Change event. + * + * @throws None + * @note Updates appearance based on state changes. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void changeEvent(QEvent* event) override; + + /** + * @brief Handles text change event. + * + * @param[in] text New text content. + * + * @throws None + * @note Updates floating label state and character counter. + * @warning None + * @since 0.1 + * @ingroup ui_widget_material_widget + */ + void textChanged(const QString& text); + + private: + // Drawing helpers - Material Design paint pipeline + void drawBackground(QPainter& p, const QRectF& fieldRect); + void drawOutline(QPainter& p, const QRectF& fieldRect); + void drawLabel(QPainter& p, const QRectF& fieldRect); + void drawText(QPainter& p, const QRectF& textRect); + void drawPrefixIcon(QPainter& p, const QRectF& textRect); + void drawSuffixIcon(QPainter& p, const QRectF& textRect); + void drawClearButton(QPainter& p, const QRectF& textRect); + void drawHelperText(QPainter& p, const QRectF& helperRect); + void drawCharacterCounter(QPainter& p, const QRectF& helperRect); + void drawFocusIndicator(QPainter& p, const QRectF& fieldRect); + void drawRipple(QPainter& p, const QRectF& fieldRect); + + // Layout helpers + QRectF fieldRect() const; + QRectF textRect() const; + QRectF helperTextRect() const; + QRectF prefixIconRect() const; + QRectF suffixIconRect() const; + QRectF clearButtonRect() const; + + // Animation helpers + void updateFloatingState(bool shouldFloat); + void animateFloatingTo(bool floating); + + // Color access methods + CFColor containerColor() const; + CFColor onContainerColor() const; + CFColor labelColor() const; + CFColor inputTextColor() const; + CFColor outlineColor() const; + CFColor focusOutlineColor() const; + CFColor errorColor() const; + CFColor helperTextColor() const; + float cornerRadius() const; + QFont inputFont() const; + QFont labelFont() const; + QFont helperFont() const; + + // Behavior components + base::MaterialWidgetBase m_material; + + // Properties + TextFieldVariant m_variant; + QString m_label; + QString m_helperText; + QString m_errorText; + QIcon m_prefixIcon; + QIcon m_suffixIcon; + bool m_showCharacterCounter; + int m_maxLength; + + // Internal state + bool m_isFloating; + bool m_hasError; + float m_floatingProgress; // 0.0 = resting, 1.0 = floating + float m_outlineWidth; // For animating outline width + bool m_hoveringClearButton; + bool m_pressingClearButton; +}; + +} // namespace cf::ui::widget::material diff --git a/ui/widget/material/widget/treeview/treeview.cpp b/ui/widget/material/widget/treeview/treeview.cpp index eb025a0e4..1ae19d81c 100644 --- a/ui/widget/material/widget/treeview/treeview.cpp +++ b/ui/widget/material/widget/treeview/treeview.cpp @@ -20,12 +20,8 @@ #include "treeview.h" #include "application_support/application.h" #include "base/device_pixel.h" -#include "cfmaterial_animation_factory.h" #include "core/token/material_scheme/cfmaterial_token_literals.h" #include "private/treeviewdelegate.h" -#include "widget/material/base/focus_ring.h" -#include "widget/material/base/ripple_helper.h" -#include "widget/material/base/state_machine.h" #include #include @@ -36,8 +32,6 @@ namespace cf::ui::widget::material { using namespace cf::ui::base; using namespace cf::ui::base::device; -using namespace cf::ui::components; -using namespace cf::ui::components::material; using namespace cf::ui::core; using namespace cf::ui::core::token::literals; using namespace cf::ui::widget::material::base; @@ -61,31 +55,18 @@ inline CFColor fallbackOnSurface() { // ============================================================================ TreeView::TreeView(QWidget* parent) - : QTreeView(parent), m_itemHeight(TreeItemHeight::Standard), - m_indentStyle(TreeIndentStyle::Material), m_showTreeLines(true), m_rootIsDecorated(true) { - // Get animation factory from Application - m_animationFactory = - cf::WeakPtr::DynamicCast(Application::animationFactory()); - - // Initialize behavior components - m_stateMachine = new StateMachine(m_animationFactory, this); - m_ripple = new RippleHelper(m_animationFactory, this); - m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); - + : QTreeView(parent), m_material(this, + MaterialWidgetBase::Config{ + .useRipple = true, + .useElevation = false, + .useFocusIndicator = true, + }), + m_itemHeight(TreeItemHeight::Standard), m_indentStyle(TreeIndentStyle::Material), + m_showTreeLines(true), m_rootIsDecorated(true) { // Initialize and set the delegate for item rendering m_delegate = new TreeViewItemDelegate(this); setItemDelegate(m_delegate); - // Set ripple mode to bounded (clipped to item bounds) - m_ripple->setMode(RippleHelper::Mode::Bounded); - - // Connect repaint signals - // QTreeView paints on viewport, so connect to viewport() for ripple updates - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), - static_cast(&QWidget::update)); - connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - static_cast(&QWidget::update)); - // Configure tree view for Material appearance setHeaderHidden(true); // Disable Qt's default branch decoration to avoid duplicate arrow rendering @@ -256,9 +237,7 @@ CFColor TreeView::onSurfaceColor() const { void TreeView::enterEvent(QEnterEvent* event) { QTreeView::enterEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverEnter(); - } + m_material.onEnterEvent(); // Initialize hovered index on enter QPoint localPos = viewport()->mapFromGlobal(QCursor::pos()); m_hoveredIndex = indexAt(localPos); @@ -268,12 +247,7 @@ void TreeView::enterEvent(QEnterEvent* event) { void TreeView::leaveEvent(QEvent* event) { QTreeView::leaveEvent(event); - if (m_stateMachine) { - m_stateMachine->onHoverLeave(); - } - if (m_ripple) { - m_ripple->onCancel(); - } + m_material.onLeaveEvent(); // Clear hovered index if (m_hoveredIndex.isValid()) { QPersistentModelIndex oldIndex = m_hoveredIndex; @@ -343,12 +317,12 @@ void TreeView::mousePressEvent(QMouseEvent* event) { } // Still handle ripple/state effects - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } - if (m_ripple) { + if (m_material.ripple()) { // Use viewport coordinates for ripple (painter draws on viewport) - m_ripple->onPress(viewportPos, QRectF(itemRect)); + m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); // Store clip rect for ripple animation m_rippleClipRect = QRectF(itemRect); } @@ -361,8 +335,8 @@ void TreeView::mousePressEvent(QMouseEvent* event) { // Non-icon area: use normal flow (selection, ripple, etc.) QTreeView::mousePressEvent(event); - if (m_stateMachine) { - m_stateMachine->onPress(event->pos()); + if (m_material.stateMachine()) { + m_material.stateMachine()->onPress(event->pos()); } // Track pressed index for ripple @@ -371,10 +345,10 @@ void TreeView::mousePressEvent(QMouseEvent* event) { m_pressedIndex = pressedIndex; m_pressPosition = event->pos(); - if (m_ripple) { + if (m_material.ripple()) { QRect itemRect = visualRect(pressedIndex); // Use viewport coordinates for ripple (painter draws on viewport) - m_ripple->onPress(viewportPos, QRectF(itemRect)); + m_material.ripple()->onPress(viewportPos, QRectF(itemRect)); // Store clip rect for ripple animation (so it continues after release) m_rippleClipRect = QRectF(itemRect); } @@ -385,11 +359,11 @@ void TreeView::mousePressEvent(QMouseEvent* event) { void TreeView::mouseReleaseEvent(QMouseEvent* event) { QTreeView::mouseReleaseEvent(event); - if (m_stateMachine) { - m_stateMachine->onRelease(); + if (m_material.stateMachine()) { + m_material.stateMachine()->onRelease(); } - if (m_ripple) { - m_ripple->onRelease(); + if (m_material.ripple()) { + m_material.ripple()->onRelease(); } m_pressedIndex = QPersistentModelIndex(); update(); @@ -397,37 +371,18 @@ void TreeView::mouseReleaseEvent(QMouseEvent* event) { void TreeView::focusInEvent(QFocusEvent* event) { QTreeView::focusInEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusIn(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusIn(); - } - update(); + m_material.onFocusIn(); } void TreeView::focusOutEvent(QFocusEvent* event) { QTreeView::focusOutEvent(event); - if (m_stateMachine) { - m_stateMachine->onFocusOut(); - } - if (m_focusIndicator) { - m_focusIndicator->onFocusOut(); - } - update(); + m_material.onFocusOut(); } void TreeView::changeEvent(QEvent* event) { QTreeView::changeEvent(event); if (event->type() == QEvent::EnabledChange) { - if (m_stateMachine) { - if (isEnabled()) { - m_stateMachine->onEnable(); - } else { - m_stateMachine->onDisable(); - } - } - update(); + m_material.onEnabledChange(isEnabled()); } } @@ -465,30 +420,30 @@ void TreeView::paintEvent(QPaintEvent* event) { // Draw ripple if active (overlay on top of items) // RippleHelper manages its own state - always try to paint - if (m_ripple && !m_rippleClipRect.isEmpty()) { - m_ripple->setColor(onSurfaceColor()); + if (m_material.ripple() && !m_rippleClipRect.isEmpty()) { + m_material.ripple()->setColor(onSurfaceColor()); QPainterPath clipPath; clipPath.addRect(m_rippleClipRect); p.save(); p.setClipPath(clipPath); - m_ripple->paint(&p, clipPath); + m_material.ripple()->paint(&p, clipPath); p.restore(); // Clear clip rect when ripple is no longer active - if (!m_ripple->hasActiveRipple()) { + if (!m_material.ripple()->hasActiveRipple()) { m_rippleClipRect = QRectF(); } } // Draw focus indicator on current index if focused - if (m_focusIndicator && hasFocus()) { + if (m_material.focusIndicator() && hasFocus()) { QModelIndex current = currentIndex(); if (current.isValid()) { QRect itemRect = visualRect(current); if (itemRect.intersects(viewport()->rect())) { QPainterPath shape; shape.addRect(itemRect); - m_focusIndicator->paint(&p, shape, onSurfaceColor()); + m_material.focusIndicator()->paint(&p, shape, onSurfaceColor()); } } } diff --git a/ui/widget/material/widget/treeview/treeview.h b/ui/widget/material/widget/treeview/treeview.h index 764b356f7..f04527dc9 100644 --- a/ui/widget/material/widget/treeview/treeview.h +++ b/ui/widget/material/widget/treeview/treeview.h @@ -15,22 +15,14 @@ #pragma once #include "base/color.h" -#include "base/include/base/weak_ptr/weak_ptr.h" -#include "cfmaterial_animation_factory.h" #include "export.h" +#include "widget/material/base/material_widget_base.h" #include #include #include namespace cf::ui::widget::material { -// Forward declarations -namespace base { -class StateMachine; -class RippleHelper; -class MdFocusIndicator; -} // namespace base - class TreeViewItemDelegate; using CFColor = cf::ui::base::CFColor; @@ -397,10 +389,7 @@ class CF_UI_EXPORT TreeView : public QTreeView { CFColor onSurfaceColor() const; // Behavior components - cf::WeakPtr m_animationFactory; - base::StateMachine* m_stateMachine; - base::RippleHelper* m_ripple; - base::MdFocusIndicator* m_focusIndicator; + base::MaterialWidgetBase m_material; // Delegate for item rendering TreeViewItemDelegate* m_delegate; From 09f7025dc5a8c5b9801b529aa8c027f238edb588 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 24 May 2026 07:32:42 +0800 Subject: [PATCH 3/4] simplified: remove the old documentations --- base/README.md | 2 + desktop/base/logger/README.md | 2 + document/HandBook/base/weak_ptr.md | 36 +- document/HandBook/base/weak_ptr_factory.md | 57 +- .../base/config_manager/02-best-practices.md | 1618 +------------ .../desktop/base/config_manager/03-faq.md | 1421 +---------- .../desktop/base/config_manager/README.md | 134 - .../desktop/base/config_manager/index.md | 56 +- document/ci/README.md | 179 -- document/ci/index.md | 45 +- .../00_phase0_project_skeleton.md | 538 +---- .../design_stage/01_phase1_hardware_probe.md | 1078 +-------- .../design_stage/02_phase2_base_library.md | 1312 +--------- .../design_stage/03_phase3_input_layer.md | 1457 +---------- document/design_stage/04_phase6_simulator.md | 1024 +------- document/design_stage/05_phase8_testing.md | 915 +------ document/design_stage/README.md | 109 - document/design_stage/index.md | 49 +- .../multi_display_backend_architecture.md | 301 +-- .../system_architecture_overview.md | 629 +---- document/development/README.md | 173 -- document/development/index.md | 46 +- ...p-Behavior-Modeling-From-Bool-To-QFlags.md | 1112 +-------- .../notes/02-Qt-Window-Behavior-Analysis.md | 994 +------- .../03-Desktop-Strategy-Pattern-Design.md | 2147 +---------------- ...04-Desktop-Behavior-System-Architecture.md | 1739 +------------ .../05-Logger-Singleton-Link-Architecture.md | 359 +-- ...olicyChain-Clang18-Miscompilation-Debug.md | 164 +- document/notes/README.md | 189 -- document/notes/index.md | 100 +- document/scripts/README.md | 65 - document/scripts/build_helpers/README.md | 122 - document/scripts/build_helpers/index.md | 101 +- document/scripts/index.md | 53 +- document/scripts/lib/bash/README.md | 85 - document/scripts/lib/bash/index.md | 65 +- document/scripts/lib/powershell/README.md | 102 - document/scripts/lib/powershell/index.md | 81 +- document/scripts/release/hooks/README.md | 40 - document/scripts/release/hooks/index.md | 32 +- document/todo/README.md | 80 - .../todo/done/00_project_skeleton_status.md | 447 ---- .../todo/done/01_hardware_probe_status.md | 435 ---- document/todo/done/02_base_library_status.md | 625 ----- document/todo/done/03_input_layer_status.md | 541 ----- document/todo/done/04_simulator_status.md | 232 -- document/todo/done/05_testing_status.md | 303 --- .../todo/done/06_infrastructure_status.md | 170 -- document/todo/done/13_widget_apps_status.md | 156 -- .../todo/done/14_display_backend_status.md | 353 --- .../done/99_ui_material_framework_status.md | 629 ----- document/todo/done/PROJECT_STATUS_REPORT.md | 515 ---- document/todo/done/SUMMARY.md | 29 + document/todo/done/index.md | 36 - .../done/milestone_01_desktop_skeleton.md | 189 -- document/todo/index.md | 42 +- scripts/README.md | 2 + scripts/lib/README.md | 2 + scripts/release/hooks/README.md | 2 + 59 files changed, 1101 insertions(+), 22418 deletions(-) delete mode 100644 document/HandBook/desktop/base/config_manager/README.md delete mode 100644 document/ci/README.md delete mode 100644 document/design_stage/README.md delete mode 100644 document/development/README.md delete mode 100644 document/notes/README.md delete mode 100644 document/scripts/README.md delete mode 100644 document/scripts/build_helpers/README.md delete mode 100644 document/scripts/lib/bash/README.md delete mode 100644 document/scripts/lib/powershell/README.md delete mode 100644 document/scripts/release/hooks/README.md delete mode 100644 document/todo/README.md delete mode 100644 document/todo/done/00_project_skeleton_status.md delete mode 100644 document/todo/done/01_hardware_probe_status.md delete mode 100644 document/todo/done/02_base_library_status.md delete mode 100644 document/todo/done/03_input_layer_status.md delete mode 100644 document/todo/done/04_simulator_status.md delete mode 100644 document/todo/done/05_testing_status.md delete mode 100644 document/todo/done/06_infrastructure_status.md delete mode 100644 document/todo/done/13_widget_apps_status.md delete mode 100644 document/todo/done/14_display_backend_status.md delete mode 100644 document/todo/done/99_ui_material_framework_status.md delete mode 100644 document/todo/done/PROJECT_STATUS_REPORT.md create mode 100644 document/todo/done/SUMMARY.md delete mode 100644 document/todo/done/index.md delete mode 100644 document/todo/done/milestone_01_desktop_skeleton.md diff --git a/base/README.md b/base/README.md index 9436154a6..a3af0071a 100644 --- a/base/README.md +++ b/base/README.md @@ -79,3 +79,5 @@ For detailed API reference, implementation notes, and usage examples, see the Ha - Memory API: [document/HandBook/api/system/memory/memory_info.md](../document/HandBook/api/system/memory/memory_info.md) - Windows CPU implementation: [document/HandBook/implementation/windows/cpu_implementation.md](../document/HandBook/implementation/windows/cpu_implementation.md) - Linux CPU implementation: [document/HandBook/implementation/linux/cpu_implementation.md](../document/HandBook/implementation/linux/cpu_implementation.md) + +> 完整 API 手册: [document/HandBook/base/overview.md](../document/HandBook/base/overview.md) diff --git a/desktop/base/logger/README.md b/desktop/base/logger/README.md index d9b700b0a..6a6189fa1 100644 --- a/desktop/base/logger/README.md +++ b/desktop/base/logger/README.md @@ -126,3 +126,5 @@ desktop/base/logger/ - [Performance](../../../document/HandBook/desktop/base/logger/performance.md) - [Best Practices](../../../document/HandBook/desktop/base/logger/best_practices.md) - [Troubleshooting](../../../document/HandBook/desktop/base/logger/troubleshooting.md) + +> 完整日志手册: [document/HandBook/desktop/base/logger/](../../../document/HandBook/desktop/base/logger/) diff --git a/document/HandBook/base/weak_ptr.md b/document/HandBook/base/weak_ptr.md index a01799d90..74d85ba25 100644 --- a/document/HandBook/base/weak_ptr.md +++ b/document/HandBook/base/weak_ptr.md @@ -87,7 +87,7 @@ assert(!weak.IsValid()); assert(weak.Get() == nullptr); ```text -这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。 +这个设计避免了 `shared_ptr` 的隐式生命周期延长问题。持有 `WeakPtr` 不会阻止对象被销毁,这也是它和 `std::weak_ptr` 的核心区别之一。同时需要注意的是,`WeakPtr` 内部持有的是指向工厂内嵌标志的裸指针(而非 `shared_ptr`),因此 `WeakPtr` 的生命周期**绝不能**超过工厂——工厂析构后再访问 `WeakPtr` 是未定义行为。 ## 类型转换 @@ -126,7 +126,7 @@ if (derived_again) { ## 线程安全考虑 -`WeakPtr` **不是线程安全的**,应该在同一个线程(或序列)中使用。"检查有效性"和"访问对象"这两步操作不是原子的: +`WeakPtr` **不是完全线程安全的**。内部的存活标志使用 `std::atomic` 实现,`IsAlive()` 和 `Invalidate()` 本身是线程安全的。但"检查有效性"和"访问对象"这两步操作不是原子的: ```cpp // 危险:多线程环境下 @@ -140,7 +140,7 @@ if (weak.IsValid()) { // 检查通过 // 或者用其他同步机制保护整个检查+访问过程 ```text -这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为项目里的使用场景大多是单线程的,不需要这个开销。 +这个限制和 `std::weak_ptr::lock()` 不一样。标准库的 `lock()` 是原子的,可以返回一个 `shared_ptr` 保证对象在使用期间存活。我们选择不提供这个功能,是因为我们的设计中对象有唯一拥有者,不存在共享所有权。此外,标志直接嵌入在工厂中(无堆分配),`WeakPtr` 只持有裸指针,无法像 `shared_ptr` 那样延长对象生命周期。 ## 手动失效 @@ -173,38 +173,20 @@ obj.InvalidateAllRefs(); // 手动失效 assert(!weak1.IsValid()); // 失效 assert(!weak2.IsValid()); // 失效 -// 失效后仍然可以创建新的弱引用 -auto weak3 = obj.GetWeakPtr(); -assert(weak3.IsValid()); // 新的弱引用有效 +// 注意:失效后不能再调用 GetWeakPtr(),会触发断言失败 +// auto weak3 = obj.GetWeakPtr(); // 断言失败! ```text -这个功能在某些场景下很有用,比如你想显式通知所有观察者对象不再可用,但又不想真的销毁对象。注意失效后创建的新弱引用是有效的,因为工厂内部会分配新的标志位。 - -## 检查外部引用 - -`WeakPtrFactory::HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class MyClass { -private: - cf::WeakPtrFactory weak_factory_{this}; - -public: - bool HasExternalReferences() const { - return weak_factory_.HasWeakPtrs(); - } -}; -```bash - -这个接口通过 `shared_ptr::use_count()` 实现,所以是 O(1) 的。如果 `use_count() > 1`,说明除了工厂自己外,还有其他地方持有弱引用标志位。这个功能在调试或内存泄漏排查时有用。 +这个功能在某些场景下很有用,比如你想显式通知所有观察者对象不再可用,但又不想真的销毁对象。与旧版实现不同,失效后**不能再创建新的弱引用**——`GetWeakPtr()` 会触发断言失败。这是因为存活标志直接嵌入在工厂中(使用 `std::atomic`),失效只是将标志设为 `false`,不会分配新的标志。如果你需要"重启"后继续创建弱引用,应该使用一个全新的工厂实例。 ## 与 std::weak_ptr 的对比 | 特性 | cf::WeakPtr | std::weak_ptr | |------|-------------|---------------| | 所有权模型 | 独占拥有者 | 引用计数 | -| 性能开销 | 极小 | 较小(引用计数) | -| 线程安全 | 不保证 | lock() 原子操作 | +| 性能开销 | 极小(裸指针 + 原子标志) | 较小(引用计数) | +| 标志存储 | 工厂内嵌,无堆分配 | 控制块堆分配 | +| 线程安全 | 标志检查原子,访问不保证 | lock() 原子操作 | | 使用场景 | 明确生命周期的对象 | 共享所有权 | | 依赖工厂 | 需要 WeakPtrFactory | 需要 shared_ptr | diff --git a/document/HandBook/base/weak_ptr_factory.md b/document/HandBook/base/weak_ptr_factory.md index 0d29e4fe5..9b1bcc988 100644 --- a/document/HandBook/base/weak_ptr_factory.md +++ b/document/HandBook/base/weak_ptr_factory.md @@ -70,7 +70,7 @@ assert(weak1.Get() == weak2.Get()); assert(weak2.Get() == weak3.Get()); ```text -每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。 +每次调用都创建一个新的 `WeakPtr` 对象,但它们共享同一个内部的"存活标志"。对象销毁或调用 `InvalidateWeakPtrs()` 后,所有弱引用同时失效。存活标志(`WeakReferenceFlag`)直接嵌入在 `WeakPtrFactory` 内部,不涉及任何堆分配。所有 `WeakPtr` 实例通过裸指针引用这个标志,因此 `WeakPtr` 的创建开销极小。 ## 手动失效 @@ -95,31 +95,9 @@ private: }; ```text -`InvalidateWeakPtrs()` 会把当前的存活标志设为失效,然后分配一个新的。失效前创建的所有弱引用都会变成无效,失效后调用 `GetWeakPtr()` 得到的新弱引用使用新的标志,所以是有效的。 +`InvalidateWeakPtrs()` 会把内部的存活标志设为失效。失效前创建的所有弱引用都会变成无效。注意,与旧版实现不同,失效后**不能再创建新的弱引用**——后续调用 `GetWeakPtr()` 会触发断言失败。这是因为标志直接嵌入在工厂中,失效操作只是将 `std::atomic` 设为 `false`,不会分配新的标志。 -这个功能在对象"重置"或"重启"时很有用——你想通知所有观察者旧的状态已经不可用,但对象本身还在。 - -## 检查外部引用 - -`HasWeakPtrs()` 可以检查是否有外部持有的弱引用: - -```cpp -class Service { -public: - void Shutdown() { - if (weak_factory_.HasWeakPtrs()) { - // 通知所有持有弱引用的地方 - NotifyShutdown(); - } - // 实际关闭服务... - } - -private: - cf::WeakPtrFactory weak_factory_{this}; -}; -```text - -这个接口通过内部的 `shared_ptr` 引用计数实现,所以是 O(1) 的。如果 `use_count() > 1`,说明有外部弱引用存在。注意这个检查不是实时的——调用完 `HasWeakPtrs()` 后,其他线程可能立即创建或销毁弱引用。 +这个功能在对象"重置"或"关闭"时很有用——你想通知所有观察者对象不再可用,但对象本身还在。如果你需要"重启"后继续创建弱引用,应该考虑使用一个全新的工厂实例。 ## 不可复制不可移动 @@ -193,23 +171,24 @@ private: }; ```text -### 延迟销毁检查 +### 单次失效模式 + +由于 `InvalidateWeakPtrs()` 后不能再创建新的弱引用,推荐的使用模式是"失效即终局"——调用一次 `InvalidateWeakPtrs()` 表示对象进入不可用状态,之后不再创建新的弱引用: ```cpp class ResourceManager { public: void UnloadResource(const std::string& id) { - // 先让弱引用失效 + // 让所有弱引用失效,之后不能再创建新的 weak_factory_.InvalidateWeakPtrs(); - // 检查是否有地方还在使用 - if (weak_factory_.HasWeakPtrs()) { - // 有外部引用,延迟销毁 - ScheduleDeferredUnload(id); - } else { - // 立即销毁 - resources_.erase(id); - } + // 安全地清理资源,所有外部弱引用已失效 + resources_.erase(id); + } + + // 注意:UnloadResource 后 GetWeakPtr() 会断言失败 + cf::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); } private: @@ -221,13 +200,15 @@ private: 第一,不要在构造函数里就把 `this` 传给其他地方。对象构造完成前,其他成员还没初始化,通过弱引用访问会导致未定义行为。`WeakPtrFactory` 的构造函数里有断言检查 `this` 非空,但不会检查构造是否完成。 -第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。如果你确实需要,重新考虑设计——为什么在析构函数里还要传递自己的引用? +第二,不要在析构函数里调用 `GetWeakPtr()`。对象已经在销毁过程中,创建新的弱引用没有意义。`WeakPtrFactory` 的析构函数会自动调用 `InvalidateWeakPtrs()`,之后 `GetWeakPtr()` 会断言失败。 + +第三,存活标志使用 `std::atomic` 实现,`IsAlive()` 和 `Invalidate()` 本身是线程安全的。但 `WeakPtr` 的"检查有效性 + 访问对象"两步操作不是原子的,仍然需要在同一线程(或序列)中使用。 -第三,`InvalidateWeakPtrs()` 不是原子的。多线程环境下如果一边失效一边创建新的弱引用,可能会出现新创建的弱引用意外失效。如果有这种需求,得自己加锁保护。 +第四,所有 `WeakPtr` 实例的生命周期不能超过工厂。因为 `WeakPtr` 持有的是指向工厂内嵌标志的裸指针(不是 `shared_ptr`),工厂析构后,任何对 `WeakPtr` 的访问都是未定义行为。这也是为什么工厂必须声明为最后一个成员——确保标志在其他成员的析构函数运行之前就失效,而不是等到工厂自身析构。 ## 与标准库的对比 -`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。 +`WeakPtrFactory` 没有标准库的直接对应物。`std::enable_shared_from_this` 提供了类似的功能,但它是配合 `shared_ptr` 使用的,而我们假设对象有明确的唯一拥有者。内部标志直接嵌入在工厂中(无堆分配),`WeakPtr` 通过裸指针引用该标志,整体开销比 `shared_ptr` 方案小得多。 如果你需要在已有代码中引入 `WeakPtrFactory`,最简单的迁移方式是把原来持有裸指针或引用的地方改成持有 `WeakPtr`。这通常不需要大幅改动调用代码,只需要在使用前检查有效性。 diff --git a/document/HandBook/desktop/base/config_manager/02-best-practices.md b/document/HandBook/desktop/base/config_manager/02-best-practices.md index 71ac45326..09720f4f8 100644 --- a/document/HandBook/desktop/base/config_manager/02-best-practices.md +++ b/document/HandBook/desktop/base/config_manager/02-best-practices.md @@ -5,12 +5,9 @@ description: ConfigStore 提供四层存储架构,每一层都有其特定的 # ConfigStore 最佳实践 -## 文档信息 - | 项目 | 内容 | |------|------| | 文档版本 | v1.0 | -| 创建日期 | 2026-03-17 | | 所属模块 | cf::config (ConfigStore) | | 前置知识 | 快速入门指南 (01-quick-start.md) | @@ -18,1570 +15,208 @@ description: ConfigStore 提供四层存储架构,每一层都有其特定的 ## 一、键命名规范 -### 1.1 分层结构建议 - -ConfigStore 使用点分隔的分层键名结构,建议采用 2-4 层的命名深度: +### 键名格式 -```bash +```text [level1].[level2].[level3].[level4] | | | | | | | +-- 具体属性名 (必需) - | | +----------- 子模块 (可选) - | +-------------------- 功能模块 (必需) - +----------------------------- 命名空间/域 (必需) -```text - -**推荐示例:** + | +----------- 子模块 (可选) + +-------------------- 功能模块 (必需) ++----------------------------- 命名空间/域 (必需) +``` -```text -app.theme.name # 应用主题名称 -app.theme.dark_mode # 主题深色模式 -ui.window.width # 窗口宽度 -ui.window.height # 窗口高度 -user.preferences.language # 用户语言偏好 -network.proxy.host # 代理主机地址 -network.proxy.port # 代理端口 -editor.font.size # 编辑器字体大小 -editor.font.family # 编辑器字体族 -```bash - -### 1.2 命名约定 - -遵循以下命名约定以确保代码一致性: +### 命名规则 -| 规则 | 说明 | 示例 | -|------|------|------| -| **小写字母** | 所有层级使用小写字母 | `app.theme.name` | -| **点分隔** | 使用 `.` 作为层级分隔符 | `ui.window.width` | -| **语义清晰** | 使用描述性名称,避免缩写 | `max_file_size` 而非 `mfs` | -| **下划线连接** | 多词属性用 `_` 连接 | `dark_mode` 而非 `darkMode` | -| **避免数字** | 除非是版本或编号 | `schema_v2` 而非 `style1` | -| **全英文** | 键名使用英文,非中文 | `background_color` 而非 `背景颜色` | +| 规则 | 说明 | 正确示例 | 错误示例 | +|------|------|----------|----------| +| 小写字母 | 所有层级使用小写 | `app.theme.name` | `App.Theme.Name` | +| 点分隔 | `.` 作为层级分隔符 | `ui.window.width` | `ui/window/width` | +| 语义清晰 | 使用描述性名称,避免缩写 | `max_file_size` | `mfs` | +| 下划线连接 | 多词属性用 `_` 连接 | `dark_mode` | `darkMode` | +| 避免数字 | 除非是版本或编号 | `schema_v2` | `style1` | +| 全英文 | 键名使用英文 | `background_color` | `背景颜色` | -### 1.3 避免冲突的建议 - -为避免键名冲突,推荐为不同模块或组件使用命名空间前缀: +### 命名空间预留 ```text -# 为每个子系统预留独立的命名空间 app.* # 应用程序级配置 ui.* # 用户界面配置 editor.* # 编辑器配置 network.* # 网络配置 database.* # 数据库配置 logger.* # 日志配置 -```text +``` -**常见命名空间:** +在代码中定义常量以避免拼写错误: ```cpp -// 在代码中定义命名空间常量 namespace ConfigKeys { constexpr std::string_view APP_PREFIX = "app"; - constexpr std::string_view UI_PREFIX = "ui"; - constexpr std::string_view EDITOR_PREFIX = "editor"; - - // 预定义的 KeyView inline constexpr KeyView APP_THEME_NAME{.group = "app.theme", .key = "name"}; inline constexpr KeyView UI_WINDOW_WIDTH{.group = "ui.window", .key = "width"}; - inline constexpr KeyView EDITOR_FONT_SIZE{.group = "editor.font", .key = "size"}; } -```yaml +``` --- ## 二、层级使用策略 -ConfigStore 提供四层存储架构,每一层都有其特定的使用场景: - -### 2.1 层级优先级图 +### 层级优先级 -```bash +```text +-----------------------------+ -| Temp 层 (优先级 3) | 内存临时数据,进程重启后丢失 +| Temp 层 (优先级 3) | 内存临时数据,进程重启后丢失 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| App 层 (优先级 2) | 应用运行时配置,会话有效 +| App 层 (优先级 2) | 应用运行时配置,会话有效 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| User 层 (优先级 1) | 用户个人偏好,跨会话持久化 +| User 层 (优先级 1) | 用户个人偏好,跨会话持久化 +-----------------------------+ - | - v 覆盖 + | 覆盖 +-----------------------------+ -| System 层 (优先级 0) | 系统默认配置,管理员维护 +| System 层 (优先级 0) | 系统默认配置,管理员维护 +-----------------------------+ -```text - -### 2.2 System 层:系统默认 - -**用途:** 存储应用程序的系统默认配置,由管理员或安装程序维护。 - -**使用场景:** -- 默认配置值 -- 系统级限制(如最大文件大小) -- 硬件相关的默认设置 -- 部署环境配置 - -**示例:** - -```cpp -// 初始化系统默认配置 (通常在安装时或首次运行时设置) -void init_system_defaults() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 系统默认配置应该设置为只读参考 - store.register_key( - Key{.full_key = "app.version", .full_description = "Application version"}, - std::string("1.0.0"), - Layer::System - ); - - store.register_key( - Key{.full_key = "file.max_size_mb", .full_description = "Max file size in MB"}, - 100, - Layer::System - ); - - store.register_key( - Key{.full_key = "performance.default_dpi", .full_description = "Default DPI setting"}, - 96, - Layer::System - ); - - store.sync(SyncMethod::Sync); // 同步写入 -} -```text - -### 2.3 User 层:用户偏好 - -**用途:** 存储用户的个人偏好设置,在用户配置文件中持久化。 - -**使用场景:** -- 主题和外观设置 -- 语言和区域设置 -- 用户习惯(窗口大小、位置) -- 功能开关 - -**示例:** - -```cpp -// 保存用户偏好设置 -void save_user_preferences(const UserPreferences& prefs) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "user.theme", .key = "name"}, - prefs.theme_name, Layer::User); - store.set(KeyView{.group = "user.theme", .key = "dark_mode"}, - prefs.dark_mode, Layer::User); - store.set(KeyView{.group = "user.interface", .key = "language"}, - prefs.language, Layer::User); - store.set(KeyView{.group = "user.interface", .key = "font_size"}, - prefs.font_size, Layer::User); - - // 异步保存,不阻塞 UI - store.sync(SyncMethod::Async); -} - -// 加载用户偏好设置 -UserPreferences load_user_preferences() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - UserPreferences prefs; - - prefs.theme_name = store.query( - KeyView{.group = "user.theme", .key = "name"}, "default"); - prefs.dark_mode = store.query( - KeyView{.group = "user.theme", .key = "dark_mode"}, false); - prefs.language = store.query( - KeyView{.group = "user.interface", .key = "language"}, "en_US"); - prefs.font_size = store.query( - KeyView{.group = "user.interface", .key = "font_size"}, 12); - - return prefs; -} -```text - -### 2.4 App 层:运行时状态 - -**用途:** 存储应用程序运行时状态,与应用程序生命周期绑定。 - -**使用场景:** -- 窗口几何状态 -- 最近打开的文件列表 -- 会话恢复数据 -- 应用程序内部状态 +``` -**示例:** +### 各层使用规则 -```cpp -// 保存窗口状态 -void save_window_state(QWidget* window) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - auto geometry = window->geometry(); - store.set(KeyView{.group = "app.window", .key = "x"}, - geometry.x(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "y"}, - geometry.y(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "width"}, - geometry.width(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "height"}, - geometry.height(), Layer::App); - store.set(KeyView{.group = "app.window", .key = "maximized"}, - window->isMaximized(), Layer::App); +| 层级 | 持久化 | 谁可修改 | 存储内容 | +|------|--------|----------|----------| +| **System** | 是 | 安装程序/管理员 | 默认配置值、系统级限制(最大文件大小)、硬件相关默认设置、部署环境配置 | +| **User** | 是 | 最终用户 | 主题/外观设置、语言/区域设置、用户习惯(窗口大小/位置)、功能开关 | +| **App** | 是 | 应用程序 | 窗口几何状态、最近打开文件列表、会话恢复数据、应用内部状态 | +| **Temp** | 否 | 应用程序/测试代码 | 单元测试临时配置、功能预览模式、会话令牌、调试标志 | - store.sync(SyncMethod::Async); -} +### 层级选择决策表 -// 恢复窗口状态 -void restore_window_state(QWidget* window) { - using namespace cf::config; - auto& store = ConfigStore::instance(); +| 场景 | 推荐层级 | +|------|---------| +| 应用默认值 / 系统限制 | System | +| 用户偏好 / 功能开关 | User | +| 窗口状态 / 会话数据 | App | +| 测试数据 / 会话令牌 | Temp | - int x = store.query(KeyView{.group = "app.window", .key = "x"}, 100); - int y = store.query(KeyView{.group = "app.window", .key = "y"}, 100); - int w = store.query(KeyView{.group = "app.window", .key = "width"}, 800); - int h = store.query(KeyView{.group = "app.window", .key = "height"}, 600); - bool maximized = store.query( - KeyView{.group = "app.window", .key = "maximized"}, false); - - window->setGeometry(x, y, w, h); - if (maximized) { - window->showMaximized(); - } -} -```text - -### 2.5 Temp 层:临时数据 - -**用途:** 存储进程生命周期内的临时数据,不持久化到磁盘。 - -**使用场景:** -- 单元测试中的临时配置 -- 功能演示/预览模式 -- 会话令牌 -- 调试标志 - -**示例:** +### 各层代码示例 ```cpp -// 使用 Temp 层进行单元测试 -TEST(ConfigStoreTest, OverrideWithTempLayer) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 设置系统默认值 - store.set(KeyView{.group = "test", .key = "value"}, - "system_default", Layer::System); - - // 使用 Temp 层覆盖 (不持久化) - store.set(KeyView{.group = "test", .key = "value"}, - "temp_override", Layer::Temp); - - // 查询返回 Temp 层的值 - auto value = store.query( - KeyView{.group = "test", .key = "value"}); - EXPECT_EQ(value, "temp_override"); - - // 清除 Temp 层后,回退到 System 层 - store.clear_layer(Layer::Temp); - value = store.query( - KeyView{.group = "test", .key = "value"}); - EXPECT_EQ(value, "system_default"); -} - -// 临时启用调试模式 -void enable_debug_mode_temporarily() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "debug", .key = "enabled"}, - true, Layer::Temp); - store.set(KeyView{.group = "debug", .key = "log_level"}, - 5, Layer::Temp); +// System 层:系统默认(安装时或首次运行时设置) +store.register_key(Key{.full_key = "file.max_size_mb", + .full_description = "Max file size in MB"}, 100, Layer::System); - // 不会持久化到磁盘 -} -```bash +// User 层:用户偏好 +store.set(KeyView{.group = "user.theme", .key = "dark_mode"}, true, Layer::User); +store.set(KeyView{.group = "user.interface", .key = "language"}, "zh_CN", Layer::User); +store.sync(SyncMethod::Async); -### 2.6 层级选择决策表 +// App 层:运行时状态 +store.set(KeyView{.group = "app.window", .key = "width"}, geometry.width(), Layer::App); +store.set(KeyView{.group = "app.window", .key = "maximized"}, window->isMaximized(), Layer::App); -| 场景 | 推荐层级 | 持久化 | 谁可修改 | -|------|---------|--------|----------| -| 应用默认值 | System | 是 | 安装程序/管理员 | -| 用户偏好 | User | 是 | 最终用户 | -| 窗口状态 | App | 是 | 应用程序 | -| 会话令牌 | Temp | 否 | 应用程序 | -| 测试数据 | Temp | 否 | 测试代码 | -| 限制配置 | System | 是 | 管理员 | -| 功能开关 | User | 是 | 最终用户 | +// Temp 层:临时数据(不持久化) +store.set(KeyView{.group = "debug", .key = "enabled"}, true, Layer::Temp); +``` --- ## 三、性能优化 -### 3.1 缓存利用技巧 - -ConfigStore 在内存中维护缓存,遵循以下最佳实践以充分利用缓存: - -**1. 优先使用优先级查询** - -```cpp -// 推荐:让 ConfigStore 自动查找最高优先级的值 -auto value = store.query(KeyView{.group = "app", .key = "timeout"}, 5000); - -// 不推荐:手动查询各层 -int value = 5000; -if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer::Temp)) { - value = *v; -} else if (auto v = store.query(KeyView{.group = "app", .key = "timeout"}, Layer::App)) { - value = *v; -} // ... -```text - -**2. 批量读取配置** - -```cpp -// 推荐:一次性读取需要的配置 -struct AppSettings { - std::string theme; - int dpi; - bool dark_mode; - double scale; - - static AppSettings load() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - return AppSettings{ - .theme = store.query( - KeyView{.group = "app.theme", .key = "name"}, "default"), - .dpi = store.query( - KeyView{.group = "app.display", .key = "dpi"}, 96), - .dark_mode = store.query( - KeyView{.group = "app.theme", .key = "dark_mode"}, false), - .scale = store.query( - KeyView{.group = "app.display", .key = "scale"}, 1.0) - }; - } -}; - -// 使用缓存的配置 -auto settings = AppSettings::load(); -apply_settings(settings); -```text - -**3. 避免频繁查询同一配置** - -```cpp -// 不推荐:每次函数调用都查询 -void render_frame() { - auto scale = ConfigStore::instance().query( - KeyView{.group = "display", .key = "scale"}, 1.0); - // 使用 scale... -} - -// 推荐:缓存配置值,监听变化 -class Renderer { -public: - Renderer() { - // 初始化时读取 - scale_ = ConfigStore::instance().query( - KeyView{.group = "display", .key = "scale"}, 1.0); - - // 监听变化 - watcher_handle_ = ConfigStore::instance().watch( - "display.scale", - [this](const Key&, const std::any*, const std::any* new_val, Layer) { - if (new_val) { - scale_ = std::any_cast(*new_val); - on_scale_changed(); - } - } - ); - } - - ~Renderer() { - ConfigStore::instance().unwatch(watcher_handle_); - } +### 缓存利用 - void render_frame() { - // 直接使用缓存的值 - double scale = scale_; - // ... - } +| 做法 | 推荐/不推荐 | 说明 | +|------|-------------|------| +| 优先级查询(省略 Layer 参数) | 推荐 | 自动返回最高优先级值 | +| 手动遍历各层查询 | 不推荐 | 冗余且低效 | +| 批量读取配置到结构体 | 推荐 | 减少 API 调用次数 | +| 在高频函数中重复 `query()` | 不推荐 | 缓存值 + 监听变化更优 | -private: - double scale_; - WatcherHandle watcher_handle_; - - void on_scale_changed() { - // 处理缩放变化 - } -}; -```text - -### 3.2 批量操作建议 - -当需要修改多个配置项时,使用手动通知策略可以批量处理变更: +### 批量操作 ```cpp -// 批量修改配置 -void update_multiple_settings() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 使用 Manual 策略,避免每次 set 都触发 Watcher - NotifyPolicy manual = NotifyPolicy::Manual; - - store.set(KeyView{.group = "ui", .key = "theme"}, "dark", Layer::User, manual); - store.set(KeyView{.group = "ui", .key = "font_size"}, 14, Layer::User, manual); - store.set(KeyView{.group = "ui", .key = "scale"}, 1.25, Layer::User, manual); - - // 一次性触发所有 Watcher - store.notify(); - - // 一次性同步到磁盘 - store.sync(SyncMethod::Async); -} -```text +// 使用 Manual 策略批量修改,一次性触发 Watcher +NotifyPolicy manual = NotifyPolicy::Manual; +store.set(KeyView{.group = "ui", .key = "theme"}, "dark", Layer::User, manual); +store.set(KeyView{.group = "ui", .key = "font_size"}, 14, Layer::User, manual); +store.set(KeyView{.group = "ui", .key = "scale"}, 1.25, Layer::User, manual); +store.notify(); +store.sync(SyncMethod::Async); +``` -### 3.3 异步持久化的使用 +### 异步持久化 -对于频繁写入的配置,使用异步持久化避免阻塞主线程: +频繁写入时使用 `SyncMethod::Async` 避免阻塞主线程;应用退出时使用 `SyncMethod::Sync` 确保数据落盘。 ```cpp -// 异步保存配置示例 -class ConfigSaver { -public: - static ConfigSaver& instance() { - static ConfigSaver instance; - return instance; - } - - void schedule_save() { - if (!save_pending_.load()) { - save_pending_.store(true); - // 延迟 500ms 后保存,避免频繁 I/O - QTimer::singleShot(500, [this]() { - if (save_pending_.load()) { - ConfigStore::instance().sync(SyncMethod::Async); - save_pending_.store(false); - } - }); - } - } - - void save_immediately() { - ConfigStore::instance().sync(SyncMethod::Sync); - save_pending_.store(false); - } +// 运行时:延迟异步保存 +ConfigStore::instance().sync(SyncMethod::Async); -private: - ConfigSaver() = default; - std::atomic save_pending_{false}; -}; - -// 使用示例 -void on_config_changed() { - ConfigStore::instance().set(...); - ConfigSaver::instance().schedule_save(); // 延迟异步保存 -} - -void on_application_exit() { - ConfigSaver::instance().save_immediately(); // 退出时立即保存 -} -```yaml +// 退出时:立即同步保存 +ConfigStore::instance().sync(SyncMethod::Sync); +``` --- ## 四、线程安全 -### 4.1 读多写少场景的最佳实践 - -ConfigStore 使用 `std::shared_mutex` 实现读写锁,非常适合读多写少的场景: +### 规则速查 -```cpp -// 多线程读取配置 - 完全并发 -class ConfigReader { -public: - int get_timeout() const { - // query() 内部使用 shared_lock,允许多个线程并发读取 - return ConfigStore::instance().query( - KeyView{.group = "network", .key = "timeout"}, 5000); - } - - std::string get_server_url() const { - return ConfigStore::instance().query( - KeyView{.group = "network", .key = "server_url"}, "localhost"); - } -}; - -// 多线程安全使用 -void worker_thread(int id) { - ConfigReader reader; - for (int i = 0; i < 1000; ++i) { - int timeout = reader.get_timeout(); - std::string url = reader.get_server_url(); - // 使用配置... - } -} -```text - -### 4.2 避免死锁的注意事项 +| 规则 | 说明 | +|------|------| +| 读并发安全 | `query()` 内部使用 `shared_lock`,多线程可并发读取 | +| 禁止在 Watcher 回调中调用 `set()` | 可能导致死锁;改用原子标志位延迟处理 | +| 持有外部锁时避免调用 ConfigStore | 先释放外部锁,再调用 ConfigStore | +| Watcher 回调应轻量 | 提取信息后入队,由主线程或专用线程处理 | -**1. 不要在 Watcher 回调中调用 ConfigStore::set()** +### 典型错误与修正 ```cpp -// 危险:可能导致死锁 -auto handle = ConfigStore::instance().watch( - "app.theme", - [](const Key&, auto, auto, Layer) { - // 危险!在回调中再次写入可能导致死锁 - ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); - } -); +// 危险:在 Watcher 回调中再次写入 +auto handle = store.watch("app.theme", [](const Key&, auto, auto, Layer) { + ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); // 死锁! +}); -// 安全:使用标志位延迟处理 +// 安全:使用原子标志延迟处理 std::atomic theme_changed{false}; -auto handle = ConfigStore::instance().watch( - "app.theme", - [&theme_changed](const Key&, auto, auto, Layer) { - theme_changed.store(true); - } -); - -// 在主循环中处理 -void main_loop() { - if (theme_changed.load()) { - theme_changed.store(false); - // 安全地处理主题变更 - ConfigStore::instance().set(KeyView{.group = "app", .key = "changed"}, true); - } -} -```text - -**2. 持有其他锁时避免调用 ConfigStore** - -```cpp -class MyClass { - std::mutex data_mutex_; - SomeData data_; - - void update_config() { - // 不推荐:在持有 data_mutex_ 时调用 ConfigStore - std::lock_guard lock(data_mutex_); - ConfigStore::instance().set(...); // 可能导致死锁 - } - - // 推荐:分离锁的范围 - void update_config_safe() { - // 先准备数据 - SomeData new_data; - { - std::lock_guard lock(data_mutex_); - new_data = data_; - } - // 释放锁后再调用 ConfigStore - ConfigStore::instance().set(...); - } -}; -```text - -### 4.3 Watcher 回调中的注意事项 - -```cpp -// Watcher 回调的最佳实践 -class SafeWatcher { -public: - void start_watching() { - handle_ = ConfigStore::instance().watch( - "app.*", - [this](const Key& k, const std::any* old_val, - const std::any* new_val, Layer layer) { - // 1. 避免在回调中执行耗时操作 - // 2. 不要在回调中直接修改 ConfigStore - // 3. 使用线程安全的方式通知主线程 - - // 提取必要的信息 - std::string key = k.full_key; - std::any new_value = new_val ? *new_val : std::any(); - - // 将事件放入队列,由主线程处理 - std::lock_guard lock(queue_mutex_); - event_queue_.push({key, new_value, layer}); - cv_.notify_one(); - } - ); - - // 启动处理线程 - worker_thread_ = std::thread(&SafeWatcher::process_events, this); - } - - ~SafeWatcher() { - { - std::lock_guard lock(queue_mutex_); - stop_ = true; - } - cv_.notify_one(); - if (worker_thread_.joinable()) { - worker_thread_.join(); - } - ConfigStore::instance().unwatch(handle_); - } - -private: - struct Event { - std::string key; - std::any value; - Layer layer; - }; - - void process_events() { - while (true) { - std::unique_lock lock(queue_mutex_); - cv_.wait(lock, [this] { - return stop_ || !event_queue_.empty(); - }); - - if (stop_) break; - - while (!event_queue_.empty()) { - auto event = event_queue_.front(); - event_queue_.pop(); - lock.unlock(); - - // 处理事件 (不在锁内) - handle_event(event); - - lock.lock(); - } - } - } - - void handle_event(const Event& event) { - // 安全地处理配置变更 - // 可以在这里安全地读取 ConfigStore - } - - WatcherHandle handle_; - std::thread worker_thread_; - std::mutex queue_mutex_; - std::condition_variable cv_; - std::queue event_queue_; - bool stop_ = false; -}; -```yaml +auto handle = store.watch("app.theme", [&theme_changed](const Key&, auto, auto, Layer) { + theme_changed.store(true); +}); +``` --- ## 五、错误处理 -### 5.1 类型转换的处理 - -ConfigStore 使用 `std::any` 和 `QVariant` 存储值,类型转换可能失败: - -```cpp -// 安全的类型转换处理 -class SafeConfigReader { -public: - // 方法 1:使用默认值 - int get_int(KeyView kv, int default_value = 0) { - try { - return ConfigStore::instance().query(kv, default_value); - } catch (const std::exception& e) { - log_error("Failed to read int config: ", e.what()); - return default_value; - } - } - - // 方法 2:使用 optional 检查 - std::optional get_double(KeyView kv) { - auto value = ConfigStore::instance().query(kv); - if (value.has_value()) { - return value; - } - // 尝试从字符串转换 - auto str_value = ConfigStore::instance().query(kv); - if (str_value) { - try { - return std::stod(*str_value); - } catch (...) { - return std::nullopt; - } - } - return std::nullopt; - } - - // 方法 3:验证范围 - int get_percentage(KeyView kv) { - int value = ConfigStore::instance().query(kv, 100); - // 限制在 0-100 范围 - if (value < 0) return 0; - if (value > 100) return 100; - return value; - } -}; -```text - -### 5.2 键验证的处理 - -```cpp -// 键名验证 -class ConfigValidator { -public: - static bool is_valid_key(const std::string& key) { - // 检查键名格式 - if (key.empty() || key.length() > 256) { - return false; - } - - // 检查只包含允许的字符 - for (char c : key) { - if (!(std::isalnum(c) || c == '_' || c == '.')) { - return false; - } - } - - return true; - } - - static bool set_safe(const KeyView& kv, const std::string& value) { - // 先验证键名 - KeyHelper helper; - Key k; - if (!helper.fromKeyViewToKey(kv, k)) { - return false; - } - - if (!is_valid_key(k.full_key)) { - return false; - } - - // 再执行设置 - return ConfigStore::instance().set(kv, value); - } -}; -```text - -### 5.3 优雅降级策略 +| 策略 | 说明 | +|------|------| +| 提供默认值 | `query(kv, default_value)` 在键不存在时返回默认值 | +| 范围校验 | 对读取的值做 min/max 裁剪,防止配置文件被手动篡改导致异常 | +| 链式降级 | 先尝试主键,再尝试备用键,最后用硬编码默认值 | ```cpp -// 配置加载的优雅降级 -class RobustConfigLoader { -public: - struct Config { - std::string server_url; - int timeout; - int retry_count; - bool enable_cache; - - static Config load_with_fallbacks() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - Config config; - - // 尝试多个可能的键名 - config.server_url = store.query( - KeyView{.group = "network", .key = "server_url"}, - // 尝试备用键名 - store.query( - KeyView{.group = "network", .key = "host"}, - // 最后使用硬编码默认值 - "localhost" - ) - ); - - // 带验证的加载 - config.timeout = load_int_with_validation( - KeyView{.group = "network", .key = "timeout"}, - 1000, // 默认值 - 100, // 最小值 - 60000 // 最大值 - ); - - config.retry_count = load_int_with_validation( - KeyView{.group = "network", .key = "retry_count"}, - 3, // 默认值 - 0, // 最小值 - 10 // 最大值 - ); - - config.enable_cache = store.query( - KeyView{.group = "network", .key = "enable_cache"}, - true // 默认启用 - ); - - return config; - } - - private: - static int load_int_with_validation(KeyView kv, int default_value, - int min_value, int max_value) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - int value = store.query(kv, default_value); - if (value < min_value) { - log_warning("Config value too low, using minimum: ", min_value); - return min_value; - } - if (value > max_value) { - log_warning("Config value too high, using maximum: ", max_value); - return max_value; - } - return value; - } - - static void log_warning(const std::string& msg, int value) { - // 实现日志记录 - } - }; -}; -```yaml +// 带范围校验的读取 +int timeout = store.query(KeyView{.group = "network", .key = "timeout"}, 1000); +timeout = std::clamp(timeout, 100, 60000); +``` --- -## 六、实践模式 - -### 6.1 模式一:配置管理器封装类 - -封装 ConfigStore 以提供类型安全的配置访问接口: - -```cpp -/** - * @brief 应用程序配置管理器 - * - * 封装 ConfigStore,提供类型安全的配置访问接口, - * 并集中管理配置键名和默认值。 - */ -class AppConfigManager { -public: - // 单例模式 - static AppConfigManager& instance() { - static AppConfigManager instance; - return instance; - } - - // ========== 主题配置 ========== - - struct ThemeConfig { - std::string name = "default"; - bool dark_mode = false; - int accent_color = 0x2196F3; - }; - - ThemeConfig get_theme() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return ThemeConfig{ - .name = store.query( - KeyView{.group = "app.theme", .key = "name"}, "default"), - .dark_mode = store.query( - KeyView{.group = "app.theme", .key = "dark_mode"}, false), - .accent_color = store.query( - KeyView{.group = "app.theme", .key = "accent_color"}, 0x2196F3) - }; - } - - void set_theme(const ThemeConfig& config) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.set(KeyView{.group = "app.theme", .key = "name"}, - config.name, Layer::User); - store.set(KeyView{.group = "app.theme", .key = "dark_mode"}, - config.dark_mode, Layer::User); - store.set(KeyView{.group = "app.theme", .key = "accent_color"}, - config.accent_color, Layer::User); - - store.sync(SyncMethod::Async); - } - - // ========== 网络配置 ========== - - struct NetworkConfig { - std::string server_url = "https://api.example.com"; - int timeout_ms = 5000; - int max_retries = 3; - bool enable_proxy = false; - std::string proxy_host; - int proxy_port = 8080; - }; - - NetworkConfig get_network() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return NetworkConfig{ - .server_url = store.query( - KeyView{.group = "network", .key = "server_url"}, - "https://api.example.com"), - .timeout_ms = clamp_range( - store.query(KeyView{.group = "network", .key = "timeout_ms"}, 5000), - 1000, 30000), - .max_retries = clamp_range( - store.query(KeyView{.group = "network", .key = "max_retries"}, 3), - 0, 10), - .enable_proxy = store.query( - KeyView{.group = "network", .key = "enable_proxy"}, false), - .proxy_host = store.query( - KeyView{.group = "network", .key = "proxy_host"}, ""), - .proxy_port = store.query( - KeyView{.group = "network", .key = "proxy_port"}, 8080) - }; - } - - // ========== 编辑器配置 ========== - - struct EditorConfig { - std::string font_family = "Monospace"; - int font_size = 12; - bool line_numbers = true; - bool word_wrap = false; - int tab_size = 4; - }; - - EditorConfig get_editor() const { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - return EditorConfig{ - .font_family = store.query( - KeyView{.group = "editor", .key = "font_family"}, "Monospace"), - .font_size = clamp_range( - store.query(KeyView{.group = "editor", .key = "font_size"}, 12), - 8, 72), - .line_numbers = store.query( - KeyView{.group = "editor", .key = "line_numbers"}, true), - .word_wrap = store.query( - KeyView{.group = "editor", .key = "word_wrap"}, false), - .tab_size = clamp_range( - store.query(KeyView{.group = "editor", .key = "tab_size"}, 4), - 1, 8) - }; - } - - // ========== 初始化 ========== - - void initialize() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - // 注册所有配置键 - register_theme_keys(); - register_network_keys(); - register_editor_keys(); - - store.sync(SyncMethod::Sync); - } - -private: - AppConfigManager() = default; - ~AppConfigManager() = default; - - // 禁止拷贝和移动 - AppConfigManager(const AppConfigManager&) = delete; - AppConfigManager& operator=(const AppConfigManager&) = delete; - - static int clamp_range(int value, int min_val, int max_val) { - if (value < min_val) return min_val; - if (value > max_val) return max_val; - return value; - } - - void register_theme_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "app.theme.name", - .full_description = "Application theme name"}, - std::string("default"), Layer::User); - - store.register_key( - Key{.full_key = "app.theme.dark_mode", - .full_description = "Enable dark mode"}, - false, Layer::User); - - store.register_key( - Key{.full_key = "app.theme.accent_color", - .full_description = "Theme accent color (ARGB)"}, - 0x2196F3, Layer::User); - } - - void register_network_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "network.server_url", - .full_description = "API server URL"}, - std::string("https://api.example.com"), Layer::User); - - store.register_key( - Key{.full_key = "network.timeout_ms", - .full_description = "Network request timeout in milliseconds"}, - 5000, Layer::User); - - store.register_key( - Key{.full_key = "network.max_retries", - .full_description = "Maximum number of retry attempts"}, - 3, Layer::User); - } - - void register_editor_keys() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - store.register_key( - Key{.full_key = "editor.font_family", - .full_description = "Editor font family"}, - std::string("Monospace"), Layer::User); - - store.register_key( - Key{.full_key = "editor.font_size", - .full_description = "Editor font size in points"}, - 12, Layer::User); - - store.register_key( - Key{.full_key = "editor.tab_size", - .full_description = "Tab size in spaces"}, - 4, Layer::User); - } -}; - -// 使用示例 -void use_app_config() { - auto& config = AppConfigManager::instance(); - - // 获取配置 - auto theme = config.get_theme(); - auto network = config.get_network(); - auto editor = config.get_editor(); - - // 修改配置 - ThemeConfig new_theme; - new_theme.name = "dark"; - new_theme.dark_mode = true; - config.set_theme(new_theme); -} -```text - -### 6.2 模式二:热重载支持 - -实现配置文件的监听和热重载功能: - -```cpp -/** - * @brief 配置热重载管理器 - * - * 监听配置文件变化,自动重新加载配置并通知观察者。 - */ -class ConfigHotReload { -public: - using ConfigChangedCallback = std::function; - - static ConfigHotReload& instance() { - static ConfigHotReload instance; - return instance; - } - - void start_watching() { - if (watcher_) return; // 已启动 - - // 使用 QFileSystemWatcher 监听配置文件 - watcher_ = std::make_unique(); - - // 添加要监听的配置文件路径 - auto paths = get_config_paths(); - for (const auto& path : paths) { - if (QFile::exists(path)) { - watcher_->addPath(path); - } - } - - // 连接文件变化信号 - QObject::connect(watcher_.get(), &QFileSystemWatcher::fileChanged, - this, &ConfigHotReload::on_file_changed); - - // 设置防抖定时器 - debounce_timer_ = std::make_unique(); - debounce_timer_->setSingleShot(true); - debounce_timer_->setInterval(500); // 500ms 防抖 - - QObject::connect(debounce_timer_.get(), &QTimer::timeout, - this, &ConfigHotReload::reload_config); - } - - void stop_watching() { - watcher_.reset(); - debounce_timer_.reset(); - } - - void add_observer(ConfigChangedCallback callback) { - observers_.push_back(std::move(callback)); - } - -private: - ConfigHotReload() = default; - ~ConfigHotReload() = default; - - QStringList get_config_paths() { - using namespace cf::config; - // 获取配置文件路径 - // 这里简化处理,实际应该从 PathProvider 获取 - return { - QString::fromStdString(get_user_config_path()), - QString::fromStdString(get_app_config_path()) - }; - } - - std::string get_user_config_path() { - // 实现获取用户配置路径 - return "~/.config/cfdesktop/user.ini"; - } - - std::string get_app_config_path() { - // 实现获取应用配置路径 - return "./config/app.ini"; - } - - void on_file_changed(const QString& path) { - // 文件变化时,启动防抖定时器 - qDebug() << "Config file changed:" << path; - debounce_timer_->start(); - } - - void reload_config() { - using namespace cf::config; - - qDebug() << "Reloading configuration..."; - - // 重新加载配置 - ConfigStore::instance().reload(); - - // 通知所有观察者 - for (auto& observer : observers_) { - observer(); - } - - qDebug() << "Configuration reloaded"; - } - - std::unique_ptr watcher_; - std::unique_ptr debounce_timer_; - std::vector observers_; -}; - -// 使用示例 -class ThemeManager { -public: - ThemeManager() { - // 监听配置变化 - ConfigHotReload::instance().add_observer([this]() { - on_config_reloaded(); - }); - } +## 六、实践模式速查 - void apply_theme() { - auto& config = AppConfigManager::instance(); - auto theme = config.get_theme(); - // 应用主题... - qDebug() << "Theme applied:" << QString::fromStdString(theme.name); - } - -private: - void on_config_reloaded() { - qDebug() << "Config reloaded, reapplying theme..."; - apply_theme(); - } -}; - -// 在应用启动时启用热重载 -void initialize_application() { - // 初始化配置 - AppConfigManager::instance().initialize(); - - // 启用热重载 (开发环境) -#ifdef QT_DEBUG - ConfigHotReload::instance().start_watching(); -#endif -} -```text - -### 6.3 模式三:配置迁移 - -处理配置版本升级时的数据迁移: - -```cpp -/** - * @brief 配置迁移管理器 - * - * 处理不同版本配置之间的数据迁移。 - */ -class ConfigMigration { -public: - static ConfigMigration& instance() { - static ConfigMigration instance; - return instance; - } - - /** - * 执行配置迁移 - * @param from_version 迁移前的版本号 - * @param to_version 迁移后的版本号 - */ - bool migrate(int from_version, int to_version) { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - for (int v = from_version; v < to_version; ++v) { - if (!migrate_to(v + 1)) { - return false; - } - } - - // 更新版本号 - store.set(KeyView{.group = "app", .key = "config_version"}, - to_version, Layer::User); - store.sync(SyncMethod::Sync); - - return true; - } - - /** - * 检查是否需要迁移 - */ - bool needs_migration() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - int current_version = store.query( - KeyView{.group = "app", .key = "config_version"}, 1); - int target_version = get_target_version(); - - return current_version < target_version; - } - - int get_current_version() const { - using namespace cf::config; - return ConfigStore::instance().query( - KeyView{.group = "app", .key = "config_version"}, 1); - } - -private: - ConfigMigration() = default; - - static constexpr int get_target_version() { return 2; } - - bool migrate_to(int version) { - switch (version) { - case 2: - return migrate_v1_to_v2(); - // 添加未来的版本迁移 - // case 3: - // return migrate_v2_to_v3(); - default: - return false; - } - } - - /** - * v1 -> v2 迁移 - * - * 变更: - * - app.theme.* 重命名为 ui.theme.* - * - editor.font.family 重命名为 editor.font_family - */ - bool migrate_v1_to_v2() { - using namespace cf::config; - auto& store = ConfigStore::instance(); - - qDebug() << "Migrating config from v1 to v2..."; - - // 手动通知策略,避免在迁移过程中触发 Watcher - NotifyPolicy manual = NotifyPolicy::Manual; - - // 迁移 app.theme.* -> ui.theme.* - migrate_key(store, "app.theme.name", "ui.theme.name", manual); - migrate_key(store, "app.theme.dark_mode", "ui.theme.dark_mode", manual); - migrate_key(store, "app.theme.accent_color", "ui.theme.accent_color", manual); - - // 迁移 editor.font.family -> editor.font_family - migrate_key(store, "editor.font.family", "editor.font_family", manual); - - // 删除旧键 - store.unregister_key(Key{.full_key = "app.theme.name", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "app.theme.dark_mode", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "app.theme.accent_color", .full_description = ""}, - Layer::User, manual); - store.unregister_key(Key{.full_key = "editor.font.family", .full_description = ""}, - Layer::User, manual); - - // 触发通知 - store.notify(); - - qDebug() << "Migration v1 -> v2 completed"; - return true; - } - - void migrate_key(cf::config::ConfigStore& store, - const std::string& old_key, - const std::string& new_key, - cf::config::NotifyPolicy policy) { - // 读取旧值 - auto old_value = get_any_value(store, old_key); - if (!old_value.has_value()) { - return; // 旧键不存在,跳过 - } - - // 写入新键 - set_any_value(store, new_key, *old_value, Layer::User, policy); - } - - std::optional get_any_value(cf::config::ConfigStore& store, - const std::string& key) { - // 简化实现,实际需要根据类型获取 - // 这里假设所有值都是字符串 - KeyView kv{.group = key.substr(0, key.rfind('.')), - .key = key.substr(key.rfind('.') + 1)}; - auto value = store.query(kv); - if (value) { - return *value; - } - return std::nullopt; - } - - void set_any_value(cf::config::ConfigStore& store, - const std::string& key, - const std::any& value, - Layer layer, - NotifyPolicy policy) { - // 简化实现 - if (value.type() == typeid(std::string)) { - KeyView kv{.group = key.substr(0, key.rfind('.')), - .key = key.substr(key.rfind('.') + 1)}; - store.set(kv, std::any_cast(value), layer, policy); - } - } -}; - -// 应用启动时执行迁移 -void check_and_migrate_config() { - auto& migration = ConfigMigration::instance(); - - if (migration.needs_migration()) { - int current = migration.get_current_version(); - int target = 2; // ConfigMigration::get_target_version() - - qDebug() << "Migrating config from v" << current << " to v" << target; - - if (migration.migrate(current, target)) { - qDebug() << "Config migration successful"; - } else { - qWarning() << "Config migration failed"; - } - } -} -```text - -### 6.4 模式四:多实例隔离(使用自定义路径提供者) - -通过自定义路径提供者实现多个 ConfigStore 实例的隔离: - -```cpp -/** - * @brief 自定义路径提供者 - * - * 为测试或特殊场景提供自定义配置文件路径。 - */ -class CustomPathProvider : public IConfigStorePathProvider { -public: - /** - * @brief 创建测试用的路径提供者 - * @param base_path 基础路径 - * @param instance_name 实例名称,用于区分不同实例 - */ - CustomPathProvider(const QString& base_path, - const QString& instance_name) - : base_path_(base_path), instance_name_(instance_name) { - - // 确保目录存在 - QDir().mkpath(base_path); - } - - QString system_path() const override { - return base_path_ + "/" + instance_name_ + "_system.ini"; - } - - QString user_dir() const override { - return base_path_; - } - - QString user_filename() const override { - return instance_name_ + "_user.ini"; - } - - QString app_dir() const override { - return base_path_; - } - - QString app_filename() const override { - return instance_name_ + "_app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - // 启用所有层 - (void)layer_index; - return true; - } - -private: - QString base_path_; - QString instance_name_; -}; - -/** - * @brief 隔离的配置存储包装器 - * - * 使用不同的配置文件路径创建隔离的配置存储实例。 - */ -class IsolatedConfigStore { -public: - explicit IsolatedConfigStore(const QString& instance_name, - const QString& base_path = "/tmp/test_config") - : instance_name_(instance_name), base_path_(base_path) { - - // 创建自定义路径提供者 - auto path_provider = std::make_shared(base_path, instance_name); - - // 初始化 ConfigStore - cf::config::ConfigStore::instance().initialize(path_provider); - } - - ~IsolatedConfigStore() { - // 可选:清理测试文件 - // cleanup(); - } - - void cleanup() { - QFile::remove(base_path_ + "/" + instance_name_ + "_system.ini"); - QFile::remove(base_path_ + "/" + instance_name_ + "_user.ini"); - QFile::remove(base_path_ + "/" + instance_name_ + "_app.ini"); - } - - cf::config::ConfigStore& store() { - return cf::config::ConfigStore::instance(); - } - -private: - QString instance_name_; - QString base_path_; -}; - -// ========== 使用示例 ========== - -// 示例 1:单元测试中的隔离配置 -class ConfigTest : public ::testing::Test { -protected: - void SetUp() override { - // 每个测试使用独立的配置文件 - test_store_ = std::make_unique( - QString("test_") + QString::number(std::rand()), // 随机实例名 - "/tmp/config_tests" - ); - } - - void TearDown() override { - test_store_->cleanup(); - } - - cf::config::ConfigStore& config() { - return test_store_->store(); - } - -private: - std::unique_ptr test_store_; -}; - -TEST_F(ConfigTest, SetValueAndQuery) { - auto& store = config(); - - store.set(cf::config::KeyView{.group = "test", .key = "value"}, - 42, cf::config::Layer::App); - - auto result = store.query(cf::config::KeyView{.group = "test", .key = "value"}); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 42); -} - -// 示例 2:多用户配置隔离 -class MultiUserConfigManager { -public: - struct UserConfig { - std::string username; - std::string theme; - int font_size; - }; - - void set_user_config(const std::string& username, const UserConfig& config) { - auto& store = get_user_store(username); - - store.set(cf::config::KeyView{.group = "user", .key = "theme"}, - config.theme, cf::config::Layer::User); - store.set(cf::config::KeyView{.group = "user", .key = "font_size"}, - config.font_size, cf::config::Layer::User); - - store.sync(cf::config::ConfigStore::SyncMethod::Sync); - } - - UserConfig get_user_config(const std::string& username) { - auto& store = get_user_store(username); - - return UserConfig{ - .username = username, - .theme = store.query( - cf::config::KeyView{.group = "user", .key = "theme"}, "default"), - .font_size = store.query( - cf::config::KeyView{.group = "user", .key = "font_size"}, 12) - }; - } - -private: - cf::config::ConfigStore& get_user_store(const std::string& username) { - // 每个用户使用独立的配置文件 - auto it = user_stores_.find(username); - if (it == user_stores_.end()) { - auto store = std::make_unique( - QString::fromStdString(username), - "/tmp/multi_user_config" - ); - it = user_stores_.emplace(username, std::move(store)).first; - } - return it->second->store(); - } - - std::unordered_map> user_stores_; -}; -```bash +| 模式 | 用途 | 关键思路 | +|------|------|----------| +| 配置管理器封装 | 类型安全的配置访问 | 单例 + 结构体封装 get/set,集中管理键名和默认值 | +| 热重载 | 开发环境监听配置文件变化 | `QFileSystemWatcher` + 防抖定时器 + `ConfigStore::reload()` | +| 配置迁移 | 版本升级时数据迁移 | 递增版本号 + `migrate_key()` 逐键迁移 + `NotifyPolicy::Manual` | +| 多实例隔离 | 测试或多用户场景 | 自定义 `IConfigStorePathProvider`,为每个实例提供独立文件路径 | --- ## 附录:快速参考 -### A.1 层级优先级速查 +### 层级优先级速查 | 层级 | 优先级 | 持久化 | 用途 | |------|--------|--------|------| @@ -1590,7 +225,7 @@ private: | User | 1 | 是 | 用户偏好 | | System | 0 (最低) | 是 | 系统默认 | -### A.2 常用代码片段 +### 常用代码片段 ```cpp // 设置用户偏好 @@ -1603,28 +238,23 @@ auto theme = ConfigStore::instance().query( // 批量修改 store.set(..., Layer::User, NotifyPolicy::Manual); -store.set(..., Layer::User, NotifyPolicy::Manual); store.notify(); store.sync(SyncMethod::Async); // 监听变化 -auto handle = store.watch("user.theme.*", [](auto... args) { - // 处理变化 -}); +auto handle = store.watch("user.theme.*", [](auto... args) { /* 处理变化 */ }); store.unwatch(handle); -```yaml +``` -### A.3 错误处理检查清单 +### 检查清单 -- [ ] 查询时是否提供合理的默认值? -- [ ] 类型转换是否考虑了失败情况? -- [ ] 键名是否进行了验证? -- [ ] Watcher 回调是否避免再次调用 ConfigStore? -- [ ] 是否使用了异步持久化避免阻塞? -- [ ] 多线程场景是否考虑了锁的顺序? +- 查询时是否提供合理的默认值? +- 类型转换是否考虑了失败情况? +- 键名是否进行了验证? +- Watcher 回调是否避免再次调用 ConfigStore? +- 是否使用了异步持久化避免阻塞? +- 多线程场景是否考虑了锁的顺序? --- -**文档版本:** v1.0 -**最后更新:** 2026-03-17 **维护者:** CFDesktop 团队 diff --git a/document/HandBook/desktop/base/config_manager/03-faq.md b/document/HandBook/desktop/base/config_manager/03-faq.md index b584b84f8..9589b3f3a 100644 --- a/document/HandBook/desktop/base/config_manager/03-faq.md +++ b/document/HandBook/desktop/base/config_manager/03-faq.md @@ -10,9 +10,7 @@ description: 本文档列出了 ConfigStore 使用中的常见问题、故障排 ## 目录 - [常见问题](#常见问题) -- [故障排查](#故障排查) - [平台特定问题](#平台特定问题) -- [性能问题](#性能问题) --- @@ -20,544 +18,219 @@ description: 本文档列出了 ConfigStore 使用中的常见问题、故障排 ### Q1: 配置修改后没有生效? -#### 可能原因 1:未调用 sync() - -配置修改后需要调用 `sync()` 才能持久化到磁盘。 +修改后必须调用 `sync()` 才能持久化到磁盘: ```cpp -// 问题代码 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); -// 程序崩溃或重启后配置丢失 - -// 解决方案:手动同步 ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); ConfigStore::instance().sync(SyncMethod::Sync); // 同步写入 // 或使用异步同步(推荐) ConfigStore::instance().sync(SyncMethod::Async); // 不阻塞调用方 -```text - -#### 可能原因 2:层级优先级问题 +``` -查询时返回的是高优先级的值,低优先级的修改被覆盖。 +如果仍不生效,检查层级优先级——查询时返回的是高优先级(Temp > App > User > System)的值: ```cpp -// 问题场景 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "light", Layer::System); +// Temp 层优先级最高,会覆盖其他层的值 ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark", Layer::Temp); +ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "light", Layer::System); +// query 返回 "dark" -// 查询时始终返回 "dark"(Temp 优先级高于 System) -auto theme = ConfigStore::instance().query( - KeyView{.group = "app", .key = "theme"}, "" -); // 返回 "dark" - -// 解决方案 1:清除高优先级配置 +// 解决方案:清除高优先级配置,或查询指定层级 ConfigStore::instance().clear_layer(Layer::Temp); - -// 解决方案 2:查询指定层级 auto system_theme = ConfigStore::instance().query( - KeyView{.group = "app", .key = "theme"}, - Layer::System, - "" + KeyView{.group = "app", .key = "theme"}, Layer::System, "" ); -```text - -#### 可能原因 3:KeyView 转换失败 - -KeyView 中包含非法字符导致键转换失败。 - -```cpp -// 问题代码 -KeyView invalid_kv{.group = "app theme", .key = "name"}; // 包含空格 -ConfigStore::instance().set(invalid_kv, "value"); // 返回 false - -// 解决方案:使用合法字符 -KeyView valid_kv{.group = "app_theme", .key = "name"}; // 仅字母、数字、下划线 -ConfigStore::instance().set(valid_kv, "value"); // 成功 -```yaml +``` --- ### Q2: 如何在不同环境使用不同配置? -#### 方法 1:使用层级分离 +使用层级分离或自定义路径提供者: ```cpp -// 开发环境 - 使用 System 层 +// 方式 1:编译期通过宏选择层级 #ifdef DEBUG ConfigStore::instance().set( KeyView{.group = "api", .key = "endpoint"}, - "https://dev.api.example.com", - Layer::System + "https://dev.api.example.com", Layer::System ); #else ConfigStore::instance().set( KeyView{.group = "api", .key = "endpoint"}, - "https://api.example.com", - Layer::System + "https://api.example.com", Layer::System ); #endif -// 用户可以覆盖(User 层优先级更高) -ConfigStore::instance().set( - KeyView{.group = "api", .key = "endpoint"}, - "https://custom.api.com", - Layer::User -); -```text - -#### 方法 2:自定义路径提供者 - -> **注**: 以下示例展示如何为自定义应用实现路径提供者。CFDesktop 默认实现使用应用自管理目录,不读取 `/etc/` 系统路径。 - -```cpp -#include -#include - +// 方式 2:自定义 IConfigStorePathProvider class EnvironmentPathProvider : public IConfigStorePathProvider { public: QString system_path() const override { -#ifdef DEBUG - return QString::fromStdString("/etc/myapp/dev/system.ini"); -#else - return QString::fromStdString("/etc/myapp/prod/system.ini"); -#endif + return QString::fromStdString("/etc/myapp/system.ini"); } - QString user_dir() const override { return QString::fromStdString(std::string(getenv("HOME")) + "/.config/myapp"); } - - QString user_filename() const override { - return "user.ini"; - } - + QString user_filename() const override { return "user.ini"; } QString app_dir() const override { return QCoreApplication::applicationDirPath() + "/config"; } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; // 启用所有层级 - } + QString app_filename() const override { return "app.ini"; } + bool is_layer_enabled(int) const override { return true; } }; - -// 初始化时使用 ConfigStore::instance().initialize(std::make_shared()); -```text - -#### 方法 3:使用环境变量 - -> **注**: 以下为自定义应用的示例。CFDesktop 默认实现使用应用自管理目录。 - -```cpp -#include -#include - -// 从环境变量读取配置路径 -const char* env_path = getenv("MYAPP_CONFIG_PATH"); -std::string config_dir = env_path ? env_path : "/etc/myapp"; - -class DynamicPathProvider : public IConfigStorePathProvider { - QString base_dir_; -public: - DynamicPathProvider(const QString& dir) : base_dir_(dir) {} - - QString system_path() const override { - return base_dir_ + "/system.ini"; - } - - QString user_dir() const override { - return QString::fromStdString(std::string(getenv("HOME")) + "/.config/myapp"); - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - return QCoreApplication::applicationDirPath() + "/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// 使用 -ConfigStore::instance().initialize( - std::make_shared(QString::fromStdString(config_dir)) -); -```yaml +``` --- ### Q3: Watcher 回调没有被触发? -#### 原因 1:使用了 Manual 通知策略但未调用 notify() +最常见原因是键模式不匹配。使用通配符确保匹配目标键: ```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.theme", - [](const Key& k, auto old, auto new_v, Layer layer) { - std::cout << "Theme changed" << std::endl; - }, - NotifyPolicy::Manual // 手动模式 -); +// 精确匹配 — 只匹配 "app.theme.name" +ConfigStore::instance().watch("app.theme.name", callback); -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark"); -// 回调不会被触发 +// 通配符 — 匹配所有 theme 相关键 +ConfigStore::instance().watch("*.theme.*", callback); -// 解决方案:手动触发通知 -ConfigStore::instance().set(KeyView{.group = "app", .key = "theme"}, "dark", Layer::App, NotifyPolicy::Manual); -ConfigStore::instance().notify(); // 触发所有 Manual Watcher -```text +// 通配符 — 匹配 app 下所有键 +ConfigStore::instance().watch("app.*", callback); +``` -#### 原因 2:键模式不匹配 +如果使用了 `NotifyPolicy::Manual`,需手动调用 `notify()` 才会触发回调: ```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.theme.name", // 精确匹配 - callback -); - -ConfigStore::instance().set(KeyView{.group = "other", .key = "theme"}, "dark"); -// 修改的是 "other.theme",不会触发 "app.theme.name" - -// 解决方案:使用通配符 -ConfigStore::instance().watch( - "*.theme.*", // 匹配所有 theme 相关键 - callback -); - -ConfigStore::instance().watch( - "app.*", // 匹配 app 下的所有键 - callback +ConfigStore::instance().set( + KeyView{.group = "app", .key = "theme"}, "dark", + Layer::App, NotifyPolicy::Manual ); -```text +ConfigStore::instance().notify(); // 手动触发所有 Manual Watcher +``` -#### 原因 3:Watcher 被提前取消 +确保 `WatcherHandle` 在需要监听的整个生命周期内保持有效: ```cpp -// 问题代码 -{ - WatcherHandle handle = ConfigStore::instance().watch("app.*", callback); - ConfigStore::instance().unwatch(handle); // 立即取消 - // 后续修改不会触发 -} - -// 解决方案:保持 handle 有效 class AppManager { WatcherHandle theme_watcher_; public: void init() { theme_watcher_ = ConfigStore::instance().watch( - "app.theme.*", - [this](const Key& k, auto old, auto new_v, Layer layer) { + "app.theme.*", [this](const Key& k, auto old, auto new_v, Layer layer) { onThemeChanged(k); } ); } - - ~AppManager() { - ConfigStore::instance().unwatch(theme_watcher_); - } + ~AppManager() { ConfigStore::instance().unwatch(theme_watcher_); } }; -```yaml +``` --- ### Q4: 类型转换失败怎么办? -#### 问题:读取的类型与存储的类型不匹配 +存储和读取的类型必须与 QVariant 兼容。如果以 `std::string` 存储,读取为 `int` 会失败: ```cpp -// 问题场景 -ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, std::string("123")); - -// 尝试读取为 int,但字符串无法直接转换 -auto result = ConfigStore::instance().query(KeyView{.group = "test", .key = "value"}, 0); -// 可能返回默认值 0 -```text +// 正确做法:存储时使用目标类型 +ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, 123); // 存为 int +int value = ConfigStore::instance().query( + KeyView{.group = "test", .key = "value"}, 0 +); // 返回 123 +``` -#### 解决方案 1:先读取字符串再转换 +如果类型不确定,先读取为字符串再手动转换: ```cpp auto str_value = ConfigStore::instance().query( KeyView{.group = "test", .key = "value"}, "" ); - if (!str_value.empty()) { try { int int_value = std::stoi(str_value); - // 使用 int_value } catch (const std::exception& e) { // 处理转换错误 } } -```text - -#### 解决方案 2:使用 QVariant 兼容的类型 - -```cpp -// 存储时使用 QVariant 能正确转换的类型 -ConfigStore::instance().set(KeyView{.group = "test", .key = "value"}, 123); // 存为 int - -// 读取时可以直接获取 -int value = ConfigStore::instance().query( - KeyView{.group = "test", .key = "value"}, 0 -); // 正确返回 123 -```text - -#### 解决方案 3:使用 std::any 处理多种类型 - -```cpp -std::any value = get_raw_value(KeyView{.group = "test", .key = "value"}); - -if (value.type() == typeid(int)) { - int int_value = std::any_cast(value); -} else if (value.type() == typeid(std::string)) { - std::string str_value = std::any_cast(value); - // 手动转换... -} -```yaml +``` --- ### Q5: 如何迁移旧的配置文件? -#### 方法 1:直接复制 INI 文件 - -```bash -# Linux -cp /old/path/config.ini ~/.config/myapp/user.ini - -# Windows -copy C:\OldPath\config.ini %APPDATA%\MyApp\user.ini - -# macOS -cp /old/path/config.plist ~/Library/Preferences/com.myapp.plist -```text - -#### 方法 2:使用 ConfigStore API 迁移 +使用 `QSettings` 读取旧配置,通过 ConfigStore API 写入新格式: ```cpp void migrate_old_config(const std::string& old_file_path) { QSettings old_settings( - QString::fromStdString(old_file_path), - QSettings::IniFormat + QString::fromStdString(old_file_path), QSettings::IniFormat ); - - // 遍历所有旧配置 for (const auto& group : old_settings.childGroups()) { old_settings.beginGroup(group); - for (const auto& key : old_settings.childKeys()) { QVariant value = old_settings.value(key); - - // 构造 KeyView - KeyView kv{ - .group = group.toStdString(), - .key = key.toStdString() - }; - - // 根据类型写入新配置 - if (value.type() == QVariant::Int) { + KeyView kv{.group = group.toStdString(), .key = key.toStdString()}; + if (value.type() == QVariant::Int) ConfigStore::instance().set(kv, value.toInt(), Layer::User); - } else if (value.type() == QVariant::String) { + else if (value.type() == QVariant::String) ConfigStore::instance().set(kv, value.toString().toStdString(), Layer::User); - } else if (value.type() == QVariant::Bool) { + else if (value.type() == QVariant::Bool) ConfigStore::instance().set(kv, value.toBool(), Layer::User); - } - // 其他类型... } - - old_settings.endGroup(); - } - - // 同步到磁盘 - ConfigStore::instance().sync(SyncMethod::Sync); -} -```text - -#### 方法 3:映射旧键名到新键名 - -```cpp -struct KeyMapping { - std::string old_group; - std::string old_key; - std::string new_group; - std::string new_key; -}; - -std::vector mappings = { - {"ui", "theme", "app.theme", "name"}, - {"ui", "dpi", "display", "dpi"}, - {"network", "server", "api", "endpoint"} -}; - -void migrate_with_mapping(const std::string& old_file) { - QSettings old_settings(QString::fromStdString(old_file), QSettings::IniFormat); - - for (const auto& mapping : mappings) { - old_settings.beginGroup(QString::fromStdString(mapping.old_group)); - QVariant value = old_settings.value(QString::fromStdString(mapping.old_key)); old_settings.endGroup(); - - if (!value.isNull()) { - KeyView new_kv{ - .group = mapping.new_group, - .key = mapping.new_key - }; - - // 根据类型写入 - if (value.type() == QVariant::String) { - ConfigStore::instance().set( - new_kv, - value.toString().toStdString(), - Layer::User - ); - } - // 其他类型... - } } - ConfigStore::instance().sync(SyncMethod::Sync); } -```yaml +``` --- ### Q6: Temp 层的数据什么时候会丢失? -#### 丢失场景 - -```cpp -// 1. 程序退出后(Temp 层不持久化) -ConfigStore::instance().set(KeyView{.group = "session", .key = "id"}, "abc123", Layer::Temp); -// 程序重启后,此数据丢失 - -// 2. 调用 reload() 后 -ConfigStore::instance().set(KeyView{.group = "temp", .key = "data"}, "value", Layer::Temp); -ConfigStore::instance().reload(); // Temp 层被清空 -// 之前的数据丢失 - -// 3. 调用 clear_layer(Layer::Temp) -ConfigStore::instance().clear_layer(Layer::Temp); -// Temp 层数据被清空 -```text - -#### 保留场景 +Temp 层为纯内存存储,在以下场景会丢失:程序退出、调用 `reload()`、调用 `clear_layer(Layer::Temp)`。`sync()` 不会清空 Temp 层。 -```cpp -// Temp 层数据在程序运行期间一直有效 -ConfigStore::instance().set(KeyView{.group = "cache", .key = "key"}, "value", Layer::Temp); - -// 程序运行期间可以正常读取 -auto value = ConfigStore::instance().query( - KeyView{.group = "cache", .key = "key"}, "" -); // 返回 "value" - -// sync() 不会清空 Temp 层 -ConfigStore::instance().sync(SyncMethod::Sync); -// Temp 层数据仍然存在 -```text - -#### 最佳实践 +适用场景:会话临时数据、调试配置、运行时缓存。需要持久化的数据请使用 User 或 App 层。 ```cpp -// Temp 层适合存储: -// 1. 会话临时数据 -ConfigStore::instance().set( - KeyView{.group = "session", .key = "id"}, - generate_session_id(), - Layer::Temp -); - -// 2. 测试/调试配置 -#ifdef DEBUG - ConfigStore::instance().set( - KeyView{.group = "debug", .key = "verbose"}, - true, - Layer::Temp - ); -#endif - -// 3. 运行时计算的缓存值 +// 会话数据 — 使用 Temp ConfigStore::instance().set( - KeyView{.group = "cache", .key = "computed_value"}, - expensive_computation(), - Layer::Temp + KeyView{.group = "session", .key = "id"}, generate_session_id(), Layer::Temp ); -// 需要持久化的数据,不要使用 Temp 层 +// 用户偏好 — 使用 User ConfigStore::instance().set( - KeyView{.group = "user", .key = "preference"}, - "dark", - Layer::User // 使用 User 或 App 层 + KeyView{.group = "user", .key = "preference"}, "dark", Layer::User ); -```yaml +``` --- ### Q7: 如何批量修改配置而不触发多次 Watcher? -#### 方法 1:使用 Manual 通知策略 +使用 `NotifyPolicy::Manual`,批量修改完成后一次性触发: ```cpp -// 问题代码:每次修改都触发 Watcher -ConfigStore::instance().set(KeyView{.group = "batch", .key = "a"}, 1); -// Watcher 被触发 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "b"}, 2); -// Watcher 被触发 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "c"}, 3); -// Watcher 被触发 - -// 解决方案:使用 Manual 策略 ConfigStore::instance().set( - KeyView{.group = "batch", .key = "a"}, 1, - Layer::App, - NotifyPolicy::Manual // 不立即触发 + KeyView{.group = "batch", .key = "a"}, 1, Layer::App, NotifyPolicy::Manual ); ConfigStore::instance().set( - KeyView{.group = "batch", .key = "b"}, 2, - Layer::App, - NotifyPolicy::Manual + KeyView{.group = "batch", .key = "b"}, 2, Layer::App, NotifyPolicy::Manual ); ConfigStore::instance().set( - KeyView{.group = "batch", .key = "c"}, 3, - Layer::App, - NotifyPolicy::Manual + KeyView{.group = "batch", .key = "c"}, 3, Layer::App, NotifyPolicy::Manual ); +ConfigStore::instance().notify(); // 一次性触发所有 Watcher +``` -// 批量修改完成后,一次性触发 -ConfigStore::instance().notify(); // 所有 Watcher 被触发一次 -```text - -#### 方法 2:使用事务模式 +也可封装为 RAII 事务: ```cpp -class ConfigTransaction { -public: - ConfigTransaction() { - // 开始事务:暂存 Watcher 状态 - } - +struct ConfigTransaction { ~ConfigTransaction() { - // 提交事务:触发 notify() ConfigStore::instance().notify(); ConfigStore::instance().sync(SyncMethod::Async); } - template void set(const KeyView& key, const T& value, Layer layer = Layer::App) { ConfigStore::instance().set(key, value, layer, NotifyPolicy::Manual); @@ -566,42 +239,17 @@ public: // 使用 { - ConfigTransaction transaction; - - transaction.set(KeyView{.group = "ui", .key = "width"}, 800); - transaction.set(KeyView{.group = "ui", .key = "height"}, 600); - transaction.set(KeyView{.group = "ui", .key = "theme"}, "dark"); - - // 事务结束,自动触发通知 -} -```text - -#### 方法 3:先取消 Watcher,批量修改后再添加 - -```cpp -// 保存 Watcher -auto callback = [](const Key& k, auto old, auto new_v, Layer layer) { - // 处理变更 -}; - -// 批量修改前取消监听 -ConfigStore::instance().unwatch(watcher_handle); - -// 批量修改 -ConfigStore::instance().set(KeyView{.group = "batch", .key = "a"}, 1); -ConfigStore::instance().set(KeyView{.group = "batch", .key = "b"}, 2); -ConfigStore::instance().set(KeyView{.group = "batch", .key = "c"}, 3); - -// 重新添加 Watcher -watcher_handle = ConfigStore::instance().watch("batch.*", callback); -```bash + ConfigTransaction tx; + tx.set(KeyView{.group = "ui", .key = "width"}, 800); + tx.set(KeyView{.group = "ui", .key = "height"}, 600); + tx.set(KeyView{.group = "ui", .key = "theme"}, "dark"); +} // 析构时自动 notify + sync +``` --- ### Q8: 配置文件位置在哪里? -#### 默认路径 - | 平台 | 层级 | 路径 | |------|------|------| | **Linux** | System | `<应用目录>/config/system.ini` | @@ -614,925 +262,58 @@ watcher_handle = ConfigStore::instance().watch("batch.*", callback); | | User | `~/Library/Preferences/com.cfdesktop.user.plist` | | | App | `<应用目录>/config/app.ini` | -#### 获取当前路径 - -```cpp -// 方法 1:通过自定义路径提供者获取 -#include -#include -#include - -class DebugPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - QString path = QCoreApplication::applicationDirPath() + "/config/system.ini"; - std::cout << "System path: " << path.toStdString() << std::endl; - return path; - } - - QString user_dir() const override { - QString path = QString::fromStdString(std::string(getenv("HOME")) + "/.config/cfdesktop"); - std::cout << "User dir: " << path.toStdString() << std::endl; - return path; - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - QString path = QCoreApplication::applicationDirPath() + "/config"; - std::cout << "App dir: " << path.toStdString() << std::endl; - return path; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// 初始化 -ConfigStore::instance().initialize(std::make_shared()); - -// 方法 2:直接检查文件是否存在 -void check_config_files() { - std::array paths = { - QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini", - std::getenv("HOME") + "/.config/cfdesktop/user.ini"s, - QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini" - }; - - for (const auto& path : paths) { - QFileInfo file(QString::fromStdString(path)); - if (file.exists()) { - std::cout << "Found: " << path << std::endl; - std::cout << " Size: " << file.size() << " bytes" << std::endl; - std::cout << " Modified: " << file.lastModified().toString().toStdString() << std::endl; - } else { - std::cout << "Not found: " << path << std::endl; - } - } -} -```yaml - ---- - -## 故障排查 - -### 问题诊断流程 - -```bash - 配置读取异常 - | - +---------------+---------------+ - | | - 返回默认值? 返回错误值? - | | - ↓ | - 键不存在? | - | | - +---+---+ | - | | - 是 否 | - | | | - ↓ ↓ | -检查键名 检查层级 | - | | | - | +-------+---------------+ - | | - | 检查文件 - | | - | +---+---+ - | | | - | 文件 权限 - | 存在 问题 - | | | - +-----------+-------+ - | - 解决问题 -```text - -### 详细诊断步骤 - -#### 步骤 1:确认键是否存在 - -```cpp -void diagnose_key(const KeyView& kv) { - std::cout << "诊断键: " << kv.group << "." << kv.key << std::endl; - - // 检查键是否有效 - KeyHelper helper; - Key k; - if (!helper.fromKeyViewToKey(kv, k)) { - std::cout << " [错误] 键名无效,包含非法字符" << std::endl; - std::cout << " 提示: 键名只能包含字母、数字、下划线和点号" << std::endl; - return; - } - std::cout << " [OK] 完整键名: " << k.full_key << std::endl; - - // 检查各层是否存在 - std::array layers = {Layer::System, Layer::User, Layer::App, Layer::Temp}; - std::array layer_names = {"System", "User", "App", "Temp"}; - - for (size_t i = 0; i < layers.size(); ++i) { - bool exists = ConfigStore::instance().has_key(kv, layers[i]); - std::cout << " [" << (exists ? "X" : " ") << "] " << layer_names[i] << " 层"; - - if (exists) { - auto value = ConfigStore::instance().query( - kv, layers[i], "" - ); - std::cout << " = \"" << value << "\""; - } - std::cout << std::endl; - } - - // 检查优先级查询结果 - auto final_value = ConfigStore::instance().query(kv, ""); - std::cout << " 最终值: \"" << final_value << "\" (优先级查询)" << std::endl; -} - -// 使用 -diagnose_key(KeyView{.group = "app.theme", .key = "name"}); -```text - -#### 步骤 2:检查配置文件 - -```cpp -void diagnose_config_files() { - std::cout << "\n=== 配置文件诊断 ===" << std::endl; - - std::array, 3> files = { - {Layer::System, QCoreApplication::applicationDirPath().toStdString() + "/config/system.ini"}, - {Layer::User, std::getenv("HOME") + "/.config/cfdesktop/user.ini"s}, - {Layer::App, QCoreApplication::applicationDirPath().toStdString() + "/config/app.ini"} - }; - - for (const auto& [layer, path] : files) { - std::cout << "\n[" << (layer == Layer::System ? "System" : - layer == Layer::User ? "User" : "App") << "] " << path << std::endl; - - QFileInfo file(QString::fromStdString(path)); - - // 检查文件是否存在 - if (!file.exists()) { - std::cout << " [警告] 文件不存在" << std::endl; - continue; - } - std::cout << " [OK] 文件存在" << std::endl; - - // 检查文件权限 - if (!file.isReadable()) { - std::cout << " [错误] 文件不可读" << std::endl; - } else { - std::cout << " [OK] 文件可读" << std::endl; - } - - if (!file.isWritable()) { - std::cout << " [警告] 文件不可写(可能无法保存配置)" << std::endl; - } else { - std::cout << " [OK] 文件可写" << std::endl; - } - - // 尝试打开文件 - QSettings settings(QString::fromStdString(path), QSettings::IniFormat); - if (settings.status() != QSettings::NoError) { - std::cout << " [错误] QSettings 错误: " - << (settings.status() == QSettings::AccessError ? "访问错误" : "格式错误") - << std::endl; - } else { - std::cout << " [OK] 文件格式正确" << std::endl; - } - - // 显示文件大小和修改时间 - std::cout << " 大小: " << file.size() << " bytes" << std::endl; - std::cout << " 修改时间: " << file.lastModified().toString().toStdString() << std::endl; - } -} -```text - -#### 步骤 3:检查 Watcher 状态 - -```cpp -void diagnose_watchers() { - std::cout << "\n=== Watcher 诊断 ===" << std::endl; - - // 添加测试 Watcher - bool triggered = false; - auto handle = ConfigStore::instance().watch( - "diagnostic.test", - [&triggered](const Key& k, auto old, auto new_v, Layer layer) { - triggered = true; - std::cout << " [OK] Watcher 被触发" << std::endl; - std::cout << " 键: " << k.full_key << std::endl; - std::cout << " 层: " << static_cast(layer) << std::endl; - }, - NotifyPolicy::Immediate - ); - - std::cout << "测试 Watcher: " << handle << std::endl; - - // 触发测试 - std::cout << "触发配置变更..." << std::endl; - ConfigStore::instance().set( - KeyView{.group = "diagnostic", .key = "test"}, - std::string("test_value"), - Layer::Temp - ); - - if (!triggered) { - std::cout << " [错误] Watcher 未被触发" << std::endl; - std::cout << " 可能原因:" << std::endl; - std::cout << " 1. Watcher 回调中抛出异常" << std::endl; - std::cout << " 2. 键名不匹配" << std::endl; - std::cout << " 3. 通知策略设置错误" << std::endl; - } - - // 清理 - ConfigStore::instance().unwatch(handle); - ConfigStore::instance().clear_layer(Layer::Temp); -} -```text - -### 日志输出分析方法 - -#### 启用详细日志 - -```cpp -// 在调试模式下,包装 ConfigStore 调用以输出日志 -class LoggingConfigStore { -public: - template - static T query(const KeyView& key, const T& default_value, Layer layer = Layer::System) { - std::cout << "[ConfigStore::query] " << key.group << "." << key.key - << " (layer: " << static_cast(layer) << ")" << std::endl; - - T value = ConfigStore::instance().query(key, layer, default_value); - - std::cout << " -> returned: " << value << std::endl; - return value; - } - - template - static bool set(const KeyView& key, const T& value, Layer layer = Layer::App) { - std::cout << "[ConfigStore::set] " << key.group << "." << key.key - << " = " << value - << " (layer: " << static_cast(layer) << ")" << std::endl; - - bool result = ConfigStore::instance().set(key, value, layer); - - std::cout << " -> result: " << (result ? "success" : "failed") << std::endl; - return result; - } -}; - -// 使用 -auto theme = LoggingConfigStore::query( - KeyView{.group = "app", .key = "theme"}, - "default", - Layer::User -); -```text - -#### 检查 pending_changes - -```cpp -void monitor_pending_changes() { - size_t before = ConfigStore::instance().pending_changes(); - std::cout << "待写入变更数: " << before << std::endl; - - // 执行一些操作 - ConfigStore::instance().set(KeyView{.group = "test", .key = "a"}, 1, Layer::App, NotifyPolicy::Manual); - ConfigStore::instance().set(KeyView{.group = "test", .key = "b"}, 2, Layer::App, NotifyPolicy::Manual); - - size_t after = ConfigStore::instance().pending_changes(); - std::cout << "待写入变更数: " << after << " (新增 " << (after - before) << ")" << std::endl; - - // 调用 notify - ConfigStore::instance().notify(); - size_t after_notify = ConfigStore::instance().pending_changes(); - std::cout << "notify 后待写入变更数: " << after_notify << std::endl; -} -```text - -### 调试技巧 - -#### 技巧 1:导出所有配置 +KeyView 中 group 和 key 只允许字母、数字、下划线,包含空格等非法字符会导致 `set()` 返回 false: ```cpp -void dump_all_config() { - std::cout << "\n=== 配置转储 ===" << std::endl; - - // 导出 Temp 层 - std::cout << "\n[Temp 层]" << std::endl; - dump_layer(Layer::Temp); - - // 导出 App 层 - std::cout << "\n[App 层]" << std::endl; - dump_layer(Layer::App); - - // 导出 User 层 - std::cout << "\n[User 层]" << std::endl; - dump_layer(Layer::User); - - // 导出 System 层 - std::cout << "\n[System 层]" << std::endl; - dump_layer(Layer::System); -} - -void dump_layer(Layer layer) { - // 由于 ConfigStore 没有遍历 API,这里需要通过 QSettings 直接读取 - std::string path = get_layer_path(layer); - QSettings settings(QString::fromStdString(path), QSettings::IniFormat); - - dump_group(settings, "", 0); -} - -void dump_group(QSettings& settings, const QString& group, int indent) { - if (!group.isEmpty()) { - settings.beginGroup(group); - } - - QString prefix = QString(indent, ' '); - - // 输出所有键值对 - for (const auto& key : settings.childKeys()) { - QVariant value = settings.value(key); - std::cout << prefix.toStdString() - << key.toStdString() - << " = " - << value.toString().toStdString() - << " (" - << value.typeName().toStdString() - << ")" - << std::endl; - } - - // 递归输出子组 - for (const auto& child : settings.childGroups()) { - std::cout << prefix.toStdString() << "[" << child.toStdString() << "]" << std::endl; - dump_group(settings, child, indent + 2); - } - - if (!group.isEmpty()) { - settings.endGroup(); - } -} -```text - -#### 技巧 2:验证类型转换 - -```cpp -void test_type_conversion(const KeyView& kv) { - std::cout << "\n类型转换测试: " << kv.group << "." << kv.key << std::endl; - - // 尝试不同类型的读取 - auto as_string = ConfigStore::instance().query(kv, ""); - auto as_int = ConfigStore::instance().query(kv, 0); - auto as_double = ConfigStore::instance().query(kv, 0.0); - auto as_bool = ConfigStore::instance().query(kv, false); - - std::cout << " as string: \"" << as_string << "\"" << std::endl; - std::cout << " as int: " << as_int << std::endl; - std::cout << " as double: " << as_double << std::endl; - std::cout << " as bool: " << (as_bool ? "true" : "false") << std::endl; -} -```text - -#### 技巧 3:Watcher 性能分析 - -```cpp -class PerformanceWatcher { -public: - PerformanceWatcher(const std::string& pattern) - : pattern_(pattern) - , call_count_(0) - , total_time_(0) - , max_time_(0) - {} - - WatcherHandle install() { - return ConfigStore::instance().watch( - pattern_, - [this](const Key& k, auto old, auto new_v, Layer layer) { - auto start = std::chrono::high_resolution_clock::now(); - - // 实际回调逻辑 - this->actual_callback(k, old, new_v, layer); - - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - - call_count_++; - total_time_ += duration.count(); - max_time_ = std::max(max_time_, duration.count()); - } - ); - } - - void print_stats() { - std::cout << "\nWatcher 性能统计: " << pattern_ << std::endl; - std::cout << " 调用次数: " << call_count_ << std::endl; - if (call_count_ > 0) { - std::cout << " 平均耗时: " << (total_time_ / call_count_) << " μs" << std::endl; - std::cout << " 最大耗时: " << max_time_ << " μs" << std::endl; - std::cout << " 总耗时: " << total_time_ << " μs" << std::endl; - } - } - -private: - void actual_callback(const Key& k, const std::any* old, const std::any* new_v, Layer layer) { - // 实际的处理逻辑 - std::cout << "Config changed: " << k.full_key << std::endl; - } - - std::string pattern_; - size_t call_count_; - uint64_t total_time_; - uint64_t max_time_; -}; - -// 使用 -auto watcher = std::make_unique("app.*"); -auto handle = watcher->install(); - -// ... 运行一段时间 ... - -watcher->print_stats(); -```yaml +KeyView invalid_kv{.group = "app theme", .key = "name"}; // 失败 +KeyView valid_kv{.group = "app_theme", .key = "name"}; // 成功 +``` --- -## 平台特定问题 - -### Windows - -#### 问题 1:注册表路径限制 - -Windows 版本使用注册表存储配置,路径长度有限制。 - -```cpp -// 问题:深层嵌套的键名可能超过注册表路径限制 -ConfigStore::instance().set( - KeyView{.group = "a.very.long.group.name.that.exceeds.registry.limit", .key = "key"}, - "value" -); // 可能失败 - -// 解决方案:使用扁平化的键名 -ConfigStore::instance().set( - KeyView{.group = "app_short", .key = "long_group_key"}, - "value" -); -```text - -#### 问题 2:注册表权限 - -某些注册表路径需要管理员权限。 - -```cpp -#include - -// 检查是否有写入权限 -bool check_registry_access() { - HKEY hKey; - LPCSTR subKey = "Software\\CFDesktop"; - - // 尝试打开注册表键 - LONG result = RegOpenKeyExA( - HKEY_LOCAL_MACHINE, // System 层使用 HKLM - subKey, - 0, - KEY_WRITE, - &hKey - ); - - if (result == ERROR_ACCESS_DENIED) { - std::cerr << "错误: 需要管理员权限写入 System 层配置" << std::endl; - std::cerr << "建议: 使用 User 层或以管理员身份运行" << std::endl; - return false; - } - - if (result == ERROR_SUCCESS) { - RegCloseKey(hKey); - return true; - } - - return false; -} -```text - -#### 问题 3:INI 格式与注册表格式差异 - -```cpp -// 如果想在 Windows 上使用 INI 文件而不是注册表 -#include -#include - -class WindowsIniPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - // 返回 INI 文件路径而不是注册表路径 - return "C:/ProgramData/CFDesktop/system.ini"; - } - - QString user_dir() const override { - char* appdata = nullptr; - size_t len = 0; - _dupenv_s(&appdata, &len, "APPDATA"); - QString path = QString::fromStdString(std::string(appdata)) + "/CFDesktop"; - free(appdata); - return path; - } - - QString user_filename() const override { - return "user.ini"; - } - - QString app_dir() const override { - return QCoreApplication::applicationDirPath() + "/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; - -// QSettings 会根据路径扩展名自动选择格式 -// .ini -> INI 格式 -// 无扩展名 -> 注册表(Windows) -```text +### Q9: KeyView 合法字符规则是什么? -### Linux - -#### 问题 1:INI 文件权限 - -```cpp -// 确保 INI 文件目录存在且可写 -void ensure_config_directory(const std::string& path) { - QFileInfo file(QString::fromStdString(path)); - QDir dir = file.absoluteDir(); - - if (!dir.exists()) { - std::cout << "创建配置目录: " << dir.absolutePath().toStdString() << std::endl; - if (!dir.mkpath(".")) { - std::cerr << "错误: 无法创建配置目录" << std::endl; - std::cerr << "请检查父目录权限" << std::endl; - } - } - - // 检查写入权限 - QFileInfo dirInfo(dir.absolutePath()); - if (!dirInfo.isWritable()) { - std::cerr << "错误: 配置目录不可写" << std::endl; - std::cerr << "路径: " << dir.absolutePath().toStdString() << std::endl; - } -} - -// 使用 -ensure_config_directory("~/.config/cfdesktop/user.ini"); -```text - -#### 问题 2:System 层路径不可写 - -CFDesktop 使用应用自管理目录,System 层配置位于应用目录下(如 `{app_dir}/system.ini`),无需 root 权限。 - -```bash -# 配置文件位于应用目录下,应用启动时自动创建 -# 例如: <应用目录>/config/system.ini -# 如果目录不存在,手动创建: -mkdir -p <应用目录>/config -```text - -#### 问题 3:INI 文件编码 - -```cpp -// 确保使用 UTF-8 编码 -QSettings settings("/path/to/config.ini", QSettings::IniFormat); -settings.setIniCodec("UTF-8"); // 设置编码 - -// 写入包含非 ASCII 字符的配置 -ConfigStore::instance().set( - KeyView{.group = "app", .key = "title"}, - u8"应用程序标题", // UTF-8 字符串字面量 - Layer::User -); -```text - -### macOS - -#### 问题 1:plist vs INI - -macOS 原生使用 plist 格式,但 ConfigStore 统一使用 INI 格式。 - -```cpp -// 如果需要与系统 plist 配置交互,可以手动转换 -void import_from_plist(const std::string& plist_path) { - QSettings plist(QString::fromStdString(plist_path), QSettings::NativeFormat); - - // 读取所有键值 - for (const auto& key : plist.allKeys()) { - QVariant value = plist.value(key); - - // 转换键名格式 - std::string full_key = key.toStdString(); - std::replace(full_key.begin(), full_key.end(), '/', '.'); - - // 分离 group 和 key - size_t pos = full_key.rfind('.'); - std::string group = (pos != std::string::npos) ? full_key.substr(0, pos) : ""; - std::string key_name = (pos != std::string::npos) ? full_key.substr(pos + 1) : full_key; - - // 写入 ConfigStore - KeyView kv{.group = group, .key = key_name}; - ConfigStore::instance().set(kv, value.toString().toStdString(), Layer::User); - } -} -```text - -#### 问题 2:macOS 特殊目录 - -```cpp -#include -#include - -class MacOSPathProvider : public IConfigStorePathProvider { -public: - QString system_path() const override { - // macOS 系统配置 - return "/Library/Preferences/com.cfdesktop.system.ini"; - } - - QString user_dir() const override { - // 用户配置目录 - char* home = getenv("HOME"); - return QString::fromStdString(std::string(home)) + "/Library/Preferences"; - } - - QString user_filename() const override { - return "com.cfdesktop.user.ini"; - } - - QString app_dir() const override { - // 应用内配置(在 Bundle 内) - return QCoreApplication::applicationDirPath() + "/../Resources/config"; - } - - QString app_filename() const override { - return "app.ini"; - } - - bool is_layer_enabled(int layer_index) const override { - return true; - } -}; -```yaml +`group` 和 `key` 字段仅允许字母、数字、下划线和点号。包含空格或其他特殊字符会导致键转换失败,`set()` 返回 `false`。 --- -## 性能问题 - -### 内存占用过高 - -#### 原因分析 - -1. **缓存无限增长**:大量配置项被缓存 -2. **Watcher 泄漏**:未正确取消的 Watcher 占用内存 -3. **大量临时配置**:Temp 层积累过多数据 +### Q10: 如何减少 sync() 的性能开销? -#### 解决方案 +避免每次写入后立即同步。批量写入后调用一次异步同步: ```cpp -#include -#include - -// 方案 1:定期清理 Temp 层 -void cleanup_temp_layer() { - std::cout << "清理 Temp 层..." << std::endl; - ConfigStore::instance().clear_layer(Layer::Temp); -} - -// 方案 2:监控内存使用 -void monitor_memory_usage() { - // 使用系统 API 获取内存使用情况 -#ifdef __linux__ - std::ifstream file("/proc/self/status"); - std::string line; - while (std::getline(file, line)) { - if (line.find("VmRSS") != std::string::npos) { - std::cout << line << std::endl; - } - } -#endif -} - -// 方案 3:限制缓存大小(需要修改 ConfigStore 实现) -// 在 ConfigStoreImpl 中添加 LRU 缓存 -```text - -### 读写缓慢 - -#### 诊断 - -```cpp -#include - -void benchmark_config_operations() { - const int iterations = 1000; - - // 写入测试 - auto write_start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) { - ConfigStore::instance().set( - KeyView{.group = "bench", .key = "key_" + std::to_string(i)}, - i, - Layer::Temp - ); - } - auto write_end = std::chrono::high_resolution_clock::now(); - auto write_duration = std::chrono::duration_cast(write_end - write_start); - - std::cout << "写入 " << iterations << " 次: " - << write_duration.count() << " μs" - << " (平均 " << (write_duration.count() / iterations) << " μs/次)" - << std::endl; - - // 读取测试 - auto read_start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < iterations; ++i) { - ConfigStore::instance().query( - KeyView{.group = "bench", .key = "key_" + std::to_string(i)}, - 0 - ); - } - auto read_end = std::chrono::high_resolution_clock::now(); - auto read_duration = std::chrono::duration_cast(read_end - read_start); - - std::cout << "读取 " << iterations << " 次: " - << read_duration.count() << " μs" - << " (平均 " << (read_duration.count() / iterations) << " μs/次)" - << std::endl; - - // 清理 - ConfigStore::instance().clear_layer(Layer::Temp); -} -```text - -#### 优化建议 - -1. **减少 sync() 调用频率** -```cpp -// 不推荐:每次写入都同步 +// 不推荐:频繁同步 I/O for (int i = 0; i < 1000; ++i) { ConfigStore::instance().set(KeyView{.group = "data", .key = std::to_string(i)}, i); - ConfigStore::instance().sync(SyncMethod::Sync); // 频繁 I/O + ConfigStore::instance().sync(SyncMethod::Sync); } -// 推荐:批量写入后同步 +// 推荐:批量写入 + 一次异步同步 for (int i = 0; i < 1000; ++i) { - ConfigStore::instance().set(KeyView{.group = "data", .key = std::to_string(i)}, i, Layer::App, NotifyPolicy::Manual); -} -ConfigStore::instance().notify(); -ConfigStore::instance().sync(SyncMethod::Async); // 异步同步 -```text - -2. **使用 Temp 层存储频繁变化的配置** -```cpp -// 频繁更新的计数器,使用 Temp 层避免频繁 I/O -for (int i = 0; i < 10000; ++i) { ConfigStore::instance().set( - KeyView{.group = "counter", .key = "value"}, - i, - Layer::Temp // 不写入磁盘 + KeyView{.group = "data", .key = std::to_string(i)}, i, + Layer::App, NotifyPolicy::Manual ); } +ConfigStore::instance().notify(); +ConfigStore::instance().sync(SyncMethod::Async); +``` -// 需要持久化时再写入 -ConfigStore::instance().set( - KeyView{.group = "counter", .key = "final_value"}, - 10000, - Layer::App -); -ConfigStore::instance().sync(SyncMethod::Sync); -```text - -### Watcher 性能问题 - -#### 问题:Watcher 回调执行时间过长 - -```cpp -// 问题代码 -ConfigStore::instance().watch( - "app.*", - [](const Key& k, auto old, auto new_v, Layer layer) { - // 耗时操作 - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // 这会阻塞所有后续的配置操作 - } -); -```text - -#### 解决方案 - -```cpp -// 方案 1:使用异步处理 -ConfigStore::instance().watch( - "app.*", - [](const Key& k, auto old, auto new_v, Layer layer) { - // 将处理任务放入队列,异步执行 - std::thread([k]() { - // 在另一个线程中处理 - handle_config_change(k); - }).detach(); - } -); - -// 方案 2:使用 Debounce(防抖) -class DebouncedWatcher { - std::string pattern_; - std::chrono::milliseconds delay_; - std::thread thread_; - std::queue queue_; - std::mutex mutex_; - std::condition_variable cv_; - bool running_ = true; - -public: - DebouncedWatcher(const std::string& pattern, std::chrono::milliseconds delay) - : pattern_(pattern), delay_(delay) - { - thread_ = std::thread([this]() { - while (running_) { - std::unique_lock lock(mutex_); - cv_.wait(lock, [this]() { return !queue_.empty() || !running_; }); - - if (!running_) break; - - if (!queue_.empty()) { - auto key = queue_.front(); - queue_.pop(); - - // 等待延迟 - lock.unlock(); - std::this_thread::sleep_for(delay_); - lock.lock(); - - // 检查是否有新元素 - if (queue_.empty()) { - // 没有新元素,执行处理 - handle_change(key); - } - } - } - }); - } - - ~DebouncedWatcher() { - running_ = false; - cv_.notify_all(); - if (thread_.joinable()) { - thread_.join(); - } - } - - WatcherHandle install() { - return ConfigStore::instance().watch( - pattern_, - [this](const Key& k, auto old, auto new_v, Layer layer) { - std::lock_guard lock(mutex_); - queue_.push(k); - cv_.notify_one(); - } - ); - } - -private: - void handle_change(const Key& k) { - std::cout << "处理配置变更: " << k.full_key << std::endl; - // 实际处理逻辑 - } -}; - -// 使用 -auto debounced_watcher = std::make_unique("app.*", std::chrono::milliseconds(100)); -debounced_watcher->install(); -```yaml +频繁变化的临时数据使用 Temp 层(不写磁盘),仅在需要持久化时写入 App/User 层。 --- -## 获取帮助 - -如果以上方法无法解决问题: +## 平台特定问题 -1. 参考 [快速入门](./01-quick-start.md) -2. 搜索或提交 Issue 到项目仓库 +| 平台 | 问题 | 原因 | 解决方案 | +|------|------|------|----------| +| Windows | 写入 System 层失败 | HKLM 需要管理员权限 | 使用 User 层或以管理员身份运行 | +| Windows | 深层嵌套键名写入失败 | 注册表路径长度限制 | 使用扁平化的 group/key 命名 | +| Windows | 想使用 INI 而非注册表 | QSettings 默认使用注册表 | 实现 `IConfigStorePathProvider`,返回 `.ini` 路径 | +| Linux | 配置目录不存在 | 首次运行未创建目录 | 应用启动时 `QDir::mkpath()` 确保目录存在 | +| Linux | INI 文件中文乱码 | 文件编码非 UTF-8 | `QSettings::setIniCodec("UTF-8")` | +| Linux | System 层写入失败 | CFDesktop 使用应用自管理目录 | 确保应用目录有写权限,无需 root | +| macOS | 想读取系统 plist | ConfigStore 统一使用 INI 格式 | 用 `QSettings(path, NativeFormat)` 读取后手动导入 | --- diff --git a/document/HandBook/desktop/base/config_manager/README.md b/document/HandBook/desktop/base/config_manager/README.md deleted file mode 100644 index 17f18c323..000000000 --- a/document/HandBook/desktop/base/config_manager/README.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: "ConfigStore - 配置管理中心" -description: ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理 ---- - -# ConfigStore - 配置管理中心 - -## 简介 - -ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理和变更监听机制。它基于 Qt 的 QSettings 实现,采用 INI 格式存储配置,支持跨平台使用。 - -## 核心特性 - -### 四层存储架构 - -ConfigStore 采用四层优先级架构,支持配置的自然覆盖: - -| 层级 | 优先级 | 读写权限 | 路径 (Linux) | 用途 | -|------|--------|----------|---------------|------| -| **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | -| **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | -| **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | -| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | - -查询时按优先级从高到低查找,写入时默认写入 App 层。 - -### 类型安全 - -通过模板 API 提供类型安全的配置访问: - -```cpp -// 查询配置 -int width = ConfigStore::instance().query( - KeyView{.group = "ui", .key = "width"}, 800); - -// 设置配置 -ConfigStore::instance().set( - KeyView{.group = "ui", .key = "width"}, 1024); -```text - -支持的基础类型: -- `int` / `unsigned` / `long` 等整数类型 -- `double` / `float` 浮点类型 -- `bool` 布尔类型 -- `std::string` 字符串类型 - -### 变更监听 - -Watcher 系统支持通配符模式匹配的变更监听: - -```cpp -// 监听所有 ui.* 的变更 -auto handle = ConfigStore::instance().watch( - "ui.*", - [](const Key& k, const std::any* old, const std::any* new_v, Layer layer) { - std::cout << "UI 配置变更: " << k.full_key << std::endl; - } -); -```text - -支持两种通知策略: -- **Immediate**: 每次变更立即触发 Watcher -- **Manual**: 需要手动调用 `notify()` 触发 - -### 线程安全 - -ConfigStore 采用读写锁设计,确保多线程环境下的安全访问: - -- **读操作**: 使用 `shared_lock`,支持多线程并发读取 -- **写操作**: 使用 `unique_lock`,独占访问 -- **延迟回调**: Watcher 回调在无锁状态下执行,避免死锁 - -### 高性能 - -- **内存缓存**: 热点数据缓存在内存中,减少 I/O -- **异步持久化**: 配置写入操作异步执行,不阻塞调用方 -- **脏标记**: 只持久化实际修改的层,减少 I/O 量 - -## 快速示例 - -```cpp -#include "cfconfig.hpp" - -using namespace cf::config; - -int main() { - // 获取单例实例 - auto& config = ConfigStore::instance(); - - // 读取配置(使用默认值) - std::string theme = config.query( - KeyView{.group = "app", .key = "theme"}, "default"); - - // 写入配置(默认写入 App 层) - config.set(KeyView{.group = "app", .key = "theme"}, std::string("dark")); - - // 监听变更 - config.watch("app.*", [](const Key& k, auto old, auto new_v, Layer layer) { - std::cout << "配置变更: " << k.full_key << std::endl; - }); - - // 同步到磁盘 - config.sync(SyncMethod::Async); - - return 0; -} -```bash - -## 文档导航 - -| 文档 | 描述 | -|------|------| -| [快速入门](./01-quick-start.md) | 从零开始使用 ConfigStore | -| [最佳实践](./02-best-practices.md) | 推荐的使用模式和设计建议 | -| [常见问题](./03-faq.md) | 问题诊断和故障排查 | -| [架构详解](./04-architecture.md) | 深入理解内部实现和设计决策 | - -## 代码示例 - -运行示例: - -```bash -cd build/example/desktop/base/config_manager -./example_usage -```text - -## 依赖模块 - -- **Qt 6.8+**: 提供 QSettings 后端存储 -- **cfbase**: 提供 SimpleSingleton 基类 - -## 相关链接 - -- **API 参考手册**: [document/desktop/base/config_manager/](../../../../../document/desktop/base/config_manager/) diff --git a/document/HandBook/desktop/base/config_manager/index.md b/document/HandBook/desktop/base/config_manager/index.md index e4bcf7851..901b30c3a 100644 --- a/document/HandBook/desktop/base/config_manager/index.md +++ b/document/HandBook/desktop/base/config_manager/index.md @@ -1,11 +1,63 @@ --- title: ConfigStore 手册 -description: ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级( +description: ConfigStore 是 CFDesktop 的四层优先级配置管理系统,支持分层存储、结构化键名管理和变更监听机制。 --- # ConfigStore 手册 -ConfigStore 是 CFDesktop 的四层优先级配置管理系统,本手册详细介绍其存储层级(Temp / App / User / System)的使用方式、JSON 配置文件格式、热加载机制以及自定义配置项的扩展方法。 +ConfigStore 是 CFDesktop 桌面框架的配置管理中心,提供分层存储、结构化键名管理和变更监听机制。基于 Qt 的 QSettings 实现,采用 INI 格式存储配置,支持跨平台使用。 + +## 四层存储架构 + +ConfigStore 采用四层优先级架构,支持配置的自然覆盖: + +| 层级 | 优先级 | 读写权限 | 路径 (Linux) | 用途 | +|------|--------|----------|---------------|------| +| **Temp** | 最高 (3) | 读写 | 内存 | 临时配置,测试用,不持久化 | +| **App** | 高 (2) | 读写 | `/config/app.ini` | 应用运行时配置 | +| **User** | 中 (1) | 读写 | `~/.config/cfdesktop/user.ini` | 用户个人配置 | +| **System** | 低 (0) | 读写 | `{app_dir}/system.ini` | 系统默认配置 (CFDesktop 自管理目录) | + +查询时按优先级从高到低查找,写入时默认写入 App 层。 + +## 快速示例 + +```cpp +#include "cfconfig.hpp" + +using namespace cf::config; + +int main() { + // 获取单例实例 + auto& config = ConfigStore::instance(); + + // 读取配置(使用默认值) + std::string theme = config.query( + KeyView{.group = "app", .key = "theme"}, "default"); + + // 写入配置(默认写入 App 层) + config.set(KeyView{.group = "app", .key = "theme"}, std::string("dark")); + + // 监听变更 + config.watch("app.*", [](const Key& k, auto old, auto new_v, Layer layer) { + std::cout << "配置变更: " << k.full_key << std::endl; + }); + + // 同步到磁盘 + config.sync(SyncMethod::Async); + + return 0; +} +``` + +## 文档导航 + +| 文档 | 描述 | +|------|------| +| [快速入门](./01-quick-start.md) | 从零开始使用 ConfigStore | +| [最佳实践](./02-best-practices.md) | 推荐的使用模式和设计建议 | +| [常见问题](./03-faq.md) | 问题诊断和故障排查 | +| [架构详解](./04-architecture.md) | 深入理解内部实现和设计决策 | --- diff --git a/document/ci/README.md b/document/ci/README.md deleted file mode 100644 index 72cc40a0c..000000000 --- a/document/ci/README.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: CFDesktop CI/CD 架构文档 -description: CFDesktop 采用多架构 Docker 容器构建方案,确保代码在不同平台上都能正确编译和运行。 ---- - -# CFDesktop CI/CD 架构文档 - -## 概述 - -CFDesktop 采用多架构 Docker 容器构建方案,确保代码在不同平台上都能正确编译和运行。 - -### 核心特性 - -- **利用现有机制**: 通过 `check_toolchain.cmake` 实现工具链选择 -- **多架构支持**: 使用 Docker `--platform` 参数支持 AMD64 和 ARM64 原生编译 -- **本地验证**: 通过 Docker 在本地进行多架构构建测试 -- **独立配置**: CI 构建与本地开发构建使用独立的配置和目录 - -## 系统架构 - -```text -开发者本地 Docker 容器构建 - │ │ - │ 修改代码 │ - │ │ │ - │ 本地测试 │ - │ │ │ - ├──────┴───────────────────────────────>│ - │ │ │ - │ │ ┌────┴────┐ - │ │ │ 选择架构 │ - │ │ └────┬────┘ - │ │ │ - │ │ ┌─────┴─────┐ - │ │ │ 启动容器 │ - │ │ └─────┬─────┘ - │ │ │ - │ │ ┌───────────┼───────────┐ - │ │ │ │ │ - │ │ ┌────▼───┐ ┌───▼────┐ ┌──▼─────┐ - │ │ │ AMD64 │ │ ARM64 │ │ ARM32 │ - │ │ │ Docker │ │ Docker │ │ Docker │ - │ │ │ + Test │ │ + Test │ │ + Test │ - │ │ └────────┘ └────────┘ └────────┘ - │ │ │ │ │ - │ │ └───────────┼───────────┘ - │ │ │ - │ │ 构建成功? - │ │ │ - │ │ ┌─────▼─────┐ - │ │ │ 验证产物 │ - │ │ └───────────┘ - │ │ - │ 继续开发 ✓ - │ - └──> 代码就绪 -```text - -## 设计理念 - -### 1. 工具链选择机制 - -项目使用 [`check_toolchain.cmake`](../../cmake/check_toolchain.cmake) 实现工具链自动选择: - -```text -配置: toolchain=linux/ci-x86_64 - ↓ -CMake: -DUSE_TOOLCHAIN=linux/ci-x86_64 - ↓ -自动解析: cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake - ↓ -设置: CMAKE_TOOLCHAIN_FILE -```bash - -### 2. 分离的架构特定工具链 - -| 架构 | 工具链文件 | Qt 路径 | 配置文件 | -|------|-----------|---------|---------| -| AMD64/x86_64 | ci-x86_64-toolchain.cmake | /opt/Qt/6.8.1/gcc_64 | build_ci_config.ini | -| ARM64/aarch64 | ci-aarch64-toolchain.cmake | /opt/Qt/6.8.1/gcc_arm64 | build_ci_aarch64_config.ini | -| ARM32/armhf | ci-armhf-toolchain.cmake | /opt/Qt/6.8.1/gcc_armhf | build_ci_armhf_config.ini | - -### 3. 多架构 Docker 原生编译 - -| 对比项 | 交叉编译方案 | 多架构容器方案(本方案) | -|--------|---------------|------------------------| -| ARM 测试 | ❌ 无法在 x86_64 主机运行 | ✅ 在 ARM64 容器中运行 | -| 工具链复杂度 | ❌ 需要交叉编译工具链 | ✅ 使用原生工具链 | -| 测试真实性 | ⚠️ 无法验证 ARM 产物 | ✅ 真实 ARM 环境测试 | -| 配置复杂度 | ⚠️ 需要配置 sysroot 等 | ✅ 使用相同的工具链文件 | - -## 实施阶段 - -CI/CD 流水线分为 5 个实施阶段,当前已完成前 3 个阶段: - -| 阶段 | 名称 | 说明 | 状态 | -|------|------|------|------| -| Phase 1 | [CI 工具链设置](toolchain-setup.md) | 创建 CI 专用工具链文件 | ✅ 已完成 | -| Phase 2 | [Docker 构建环境](docker-environment.md) | 创建多架构 Dockerfile | ✅ 已完成 | -| Phase 3 | [CI 构建入口](ci-build-entry.md) | 创建统一构建脚本 | ✅ 已完成 | -| Phase 4 | GitHub Actions | 配置自动化工作流 | ⏭️ 跳过(不在远端构建) | -| Phase 5 | 异步合并机制 | 实现 pre-push hook | ⏳ 待实施 | - -## 文档导航 - -### Phase 1: CI 工具链设置 -- **[CI 工具链设置指南](toolchain-setup.md)** - 工具链文件设计和使用说明 - -### Phase 2: Docker 构建环境 -- **[Docker 构建环境设置指南](docker-environment.md)** - Docker 镜像和使用说明 - -### Phase 3: CI 构建入口 -- **[CI 构建入口设置指南](ci-build-entry.md)** - 构建脚本和配置说明 - -## 相关文件 - -### 工具链文件 - -| 文件路径 | 说明 | -|----------|------| -| [cmake/check_toolchain.cmake](../../cmake/check_toolchain.cmake) | 工具链选择机制 | -| [cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-x86_64-toolchain.cmake) | AMD64 CI 工具链 | -| [cmake/cmake_toolchain/linux/ci-aarch64-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-aarch64-toolchain.cmake) | ARM64 CI 工具链 | -| [cmake/cmake_toolchain/linux/ci-armhf-toolchain.cmake](../../cmake/cmake_toolchain/linux/ci-armhf-toolchain.cmake) | ARM32 CI 工具链 (IMX6ULL) | - -### 配置文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/build_helpers/build_ci_config.ini](../../scripts/build_helpers/build_ci_config.ini) | AMD64 CI 配置 | -| [scripts/build_helpers/build_ci_aarch64_config.ini](../../scripts/build_helpers/build_ci_aarch64_config.ini) | ARM64 CI 配置 | -| [scripts/build_helpers/build_ci_armhf_config.ini](../../scripts/build_helpers/build_ci_armhf_config.ini) | ARM32 CI 配置 | - -### 脚本文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/build_helpers/ci_build_entry.sh](../../scripts/build_helpers/ci_build_entry.sh) | CI 构建入口脚本 | -| [scripts/build_helpers/docker_start.sh](../../scripts/build_helpers/docker_start.sh) | Linux Docker 启动脚本 | -| [scripts/build_helpers/docker_start.ps1](../../scripts/build_helpers/docker_start.ps1) | Windows Docker 启动脚本 | -| [scripts/dependency/install_build_dependencies.sh](../../scripts/dependency/install_build_dependencies.sh) | 依赖安装脚本 | - -### Docker 文件 - -| 文件路径 | 说明 | -|----------|------| -| [scripts/docker/Dockerfile.build](../../scripts/docker/Dockerfile.build) | 多架构 Docker 镜像定义 | -| [scripts/docker/docker-compose.yml](../../scripts/docker/docker-compose.yml) | Docker Compose 配置 | -| [scripts/docker/.dockerignore](../../scripts/docker/.dockerignore) | Docker 忽略规则 | - -## 快速开始 - -### Docker 快速验证 - -```bash -# AMD64 构建 -bash scripts/build_helpers/docker_start.sh --verify - -# ARM64 构建 -bash scripts/build_helpers/docker_start.sh --arch arm64 --verify -```text - -### 直接运行构建 - -```bash -# 在 Linux 环境中直接运行 -bash scripts/build_helpers/ci_build_entry.sh ci -```text - -### 查看完整文档 - -```bash -# 查看完整的流水线设计文档 -cat PIPELINE.md -```yaml - ---- - -*文档版本: v2.0 | 最后更新: 2026-03-07* diff --git a/document/ci/index.md b/document/ci/index.md index 8834f9d2e..a78540d75 100644 --- a/document/ci/index.md +++ b/document/ci/index.md @@ -1,11 +1,48 @@ --- title: CI/CD -description: CFDesktop 当前使用 GitHub Actions 分层验证。 +description: CFDesktop 当前使用 GitHub Actions 分层验证,并支持多架构 Docker 容器构建。 --- # CI/CD -CFDesktop 当前使用 GitHub Actions 分层验证。 +CFDesktop 采用多架构 Docker 容器构建方案,通过 `check_toolchain.cmake` 实现工具链选择,确保代码在 AMD64 / ARM64 / ARM32 平台上正确编译和运行。 + +## CI 架构 + +### 工具链与平台 + +项目为每种目标架构提供独立的工具链文件和构建配置: + +| 架构 | 工具链文件 | Qt 路径 | +|------|-----------|---------| +| AMD64/x86_64 | `ci-x86_64-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_64` | +| ARM64/aarch64 | `ci-aarch64-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_arm64` | +| ARM32/armhf | `ci-armhf-toolchain.cmake` | `/opt/Qt/6.8.1/gcc_armhf` | + +多架构容器方案使用 Docker `--platform` 参数实现原生编译,无需交叉编译工具链,可在对应架构容器中直接运行测试。 + +### Docker 快速验证 + +```bash +# AMD64 构建 +bash scripts/build_helpers/docker_start.sh --verify + +# ARM64 构建 +bash scripts/build_helpers/docker_start.sh --arch arm64 --verify + +# 直接运行 CI 构建 +bash scripts/build_helpers/ci_build_entry.sh ci +``` + +## 实施阶段 + +| 阶段 | 名称 | 说明 | 状态 | +|------|------|------|------| +| Phase 1 | [CI 工具链设置](toolchain-setup.md) | 创建 CI 专用工具链文件 | ✅ 已完成 | +| Phase 2 | [Docker 构建环境](docker-environment.md) | 创建多架构 Dockerfile | ✅ 已完成 | +| Phase 3 | [CI 构建入口](ci-build-entry.md) | 创建统一构建脚本 | ✅ 已完成 | +| Phase 4 | GitHub Actions | 配置自动化工作流 | ⏭️ 跳过 | +| Phase 5 | 异步合并机制 | 实现 pre-push hook | ⏳ 待实施 | ## 工作流 @@ -21,3 +58,7 @@ CFDesktop 当前使用 GitHub Actions 分层验证。 - 文档相关 PR: 打 `build-doc` 标签触发文档构建。 - `develop -> main`: 必须通过 C++ build/test 和 docs build。 - `main`: 合入后发布文档站。 + +--- + +*最后更新: 2026-05-23* diff --git a/document/design_stage/00_phase0_project_skeleton.md b/document/design_stage/00_phase0_project_skeleton.md index 2b730dfe5..8cc8ec330 100644 --- a/document/design_stage/00_phase0_project_skeleton.md +++ b/document/design_stage/00_phase0_project_skeleton.md @@ -1,534 +1,28 @@ --- title: "Phase 0: 工程骨架搭建详细设计文档" -description: "- \"xaver.clang-tidy\"" +description: "项目骨架:三层目录结构、CMake 构建系统、CI/CD 流水线的设计意图" --- -# Phase 0: 工程骨架搭建详细设计文档 +# Phase 0: 工程骨架搭建 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 0 - 工程骨架 | -| 预计周期 | 1~2 周 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立项目的工程化基础,确保后续所有开发工作在稳固的架构上进行。 - -### 1.2 具体交付物 -- [ ] 完整的 CMake 构建体系 -- [ ] 三层模块目录结构(base/sdk/shell) -- [ ] 跨平台 CI/CD 流水线 -- [ ] 开发环境配置指南 -- [ ] 代码规范与格式化配置 - ---- - -## 三、CMake 构建系统设计 - -### 3.1 主 CMakeLists.txt 结构 - -```cmake -cmake_minimum_required(VERSION 3.24) -project(CFDesktop - VERSION 0.1.0 - DESCRIPTION "CFDesktop - Qt-based Embedded Desktop System" - LANGUAGES CXX -) - -# ==================== 基础配置 ==================== -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# 导出编译命令(用于 IDE 支持) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# ==================== 依赖项 ==================== -find_package(Qt6 REQUIRED COMPONENTS - Core - Gui - Widgets - Multimedia - Network -) - -# ==================== 子目录 ==================== -add_subdirectory(cmake) -add_subdirectory(src/base) -add_subdirectory(src/sdk) -add_subdirectory(src/shell) -add_subdirectory(src/simulator) -add_subdirectory(tests) - -# ==================== 打包配置 ==================== -include(CPack) -```text - -### 3.2 Base 库 CMakeLists.txt - -```cmake -project(CFDesktopBase) - -# ==================== 源文件 ==================== -set(BASE_PUBLIC_HEADERS - include/CFDesktop/Base/HardwareProbe/HardwareProbe> - include/CFDesktop/Base/HardwareProbe/HWTier> - include/CFDesktop/Base/ThemeEngine/ThemeEngine> - include/CFDesktop/Base/AnimationManager/AnimationManager> - include/CFDesktop/Base/DPIManager/DPIManager> - include/CFDesktop/Base/ConfigStore/ConfigStore> - include/CFDesktop/Base/Logger/Logger> -) - -set(BASE_SOURCES - src/hardware/HardwareProbe.cpp - src/theme/ThemeEngine.cpp - src/animation/AnimationManager.cpp - src/dpi/DPIManager.cpp - src/config/ConfigStore.cpp - src/logging/Logger.cpp -) - -# ==================== 库目标 ==================== -add_library(CFDesktopBase STATIC - ${BASE_PUBLIC_HEADERS} - ${BASE_SOURCES} -) - -# ==================== 编译选项 ==================== -target_compile_features(CFDesktopBase PUBLIC cxx_std_23) -target_include_directories(CFDesktopBase PUBLIC - $ - $ -) - -target_link_libraries(CFDesktopBase PUBLIC - Qt6::Core - Qt6::Gui -) - -# ==================== 平台特定配置 ==================== -if(UNIX AND NOT APPLE) - target_link_libraries(CFDesktopBase PUBLIC pthread dl) -endif() - -# ==================== 安装规则 ==================== -install(TARGETS CFFesktopBase - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib -) - -install(FILES ${BASE_PUBLIC_HEADERS} - DESTINATION include/CFDesktop/Base -) -```text - -### 3.3 SDK 库 CMakeLists.txt - -```cmake -project(CFDesktopSDK) - -# 依赖 Base 库 -target_link_libraries(CFDesktopSDK PUBLIC CFDesktopBase) - -# 导出 CMake 配置 -include(CMakePackageConfigHelpers) -write_basic_package_version_file( - CFDesktopSDKConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) - -install(FILES - cmake/CFDesktopSDKConfig.cmake - cmake/CFDesktopSDKConfigVersion.cmake - DESTINATION lib/cmake/CFDesktopSDK -) -```text - -### 3.4 Shell 主程序 CMakeLists.txt - -```cmake -project(CFDesktopShell) - -add_executable(cfdesktop-shell - src/main.cpp - # ... 其他源文件 -) - -target_link_libraries(cfdesktop-shell PRIVATE - CFDesktopSDK - Qt6::Widgets - Qt6::Multimedia -) - -install(TARGETS cfdesktop-shell - RUNTIME DESTINATION bin -) -```text - -### 3.5 交叉编译工具链配置 - -#### 3.5.1 ARMv7 (IMX6ULL) 工具链 - -```cmake -# cmake/toolchains/arm-linux-gnueabihf.cmake - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR arm) - -set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) -set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) - -set(CMAKE_FIND_ROOT_PATH /opt/arm-sfm-toolchain) -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(Qt6_DIR /opt/qt6-arm/lib/cmake/Qt6) -```text - -#### 3.5.2 ARM64 (RK3568/RK3588) 工具链 - -```cmake -# cmake/toolchains/aarch64-linux-gnu.cmake - -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR aarch64) - -set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) -set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) - -set(CMAKE_FIND_ROOT_PATH /opt/arm-hf-toolchain) -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(Qt6_DIR /opt/qt6-arm64/lib/cmake/Qt6) -```text - -### 3.6 CMakePresets.json - -```json -{ - "version": 3, - "cmakeMinimumRequired": { - "major": 3, - "minor": 24, - "patch": 0 - }, - "configurePresets": [ - { - "name": "linux-default", - "displayName": "Linux x64", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-default", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "BUILD_TESTING": "ON" - } - }, - { - "name": "linux-arm-sf", - "displayName": "ARMv7 Software Rendering", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-arm-sf", - "toolchainFile": "${sourceDir}/cmake/toolchains/arm-linux-gnueabihf.cmake", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "QT_PLATFORM": "linuxfb" - } - }, - { - "name": "linux-arm-hf", - "displayName": "ARM64 Hardware Accelerated", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/linux-arm-hf", - "toolchainFile": "${sourceDir}/cmake/toolchains/aarch64-linux-gnu.cmake", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "QT_PLATFORM": "eglfs" - } - } - ], - "buildPresets": [ - { - "name": "linux-default-debug", - "configurePreset": "linux-default", - "configuration": "Debug" - } - ] -} -```yaml - ---- - -## 四、CI/CD 流水线设计 - -### 4.1 主构建流程 (.github/workflows/build.yml) - -```yaml -name: Build Matrix - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - build-linux-x64: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-linux-x64:latest +CFDesktop 需要同时支持 x86_64 开发机和 ARM 嵌入式板卡(IMX6ULL/RK3568/RK3588),因此构建系统必须在第一天就具备交叉编译能力。我们选择 CMake 作为构建基础,搭配 Ninja 后端和 CMakePresets.json,让开发者通过一条 preset 命令即可切换目标平台,避免手动维护多套 Makefile。 - steps: - - uses: actions/checkout@v3 +目录结构采用三层单向依赖(`desktop/ -> ui/ -> base/ -> Qt/OS API`),这是整个项目最重要的架构约束。每一层只允许依赖下层,禁止反向引用。这种严格分层确保了 `base/` 可以独立测试和发布,`ui/` 可以在不同桌面环境中复用,而 `desktop/` 作为最终集成层可以自由组合下层能力。 - - name: Configure CMake - run: | - cmake -B build -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_TESTING=ON +CI/CD 采用 GitHub Actions 矩阵构建,在 Linux x64、ARMv7、ARM64 三个目标上并行编译,并集成 clang-tidy 静态分析和 clang-format 格式检查。所有代码质量门禁通过 Git pre-commit hook 在本地前置拦截,减少 CI 反馈周期。 - - name: Build - run: cmake --build build --parallel +## 关键决策 - - name: Run Tests - run: ctest --test-dir build --output-on-failure - - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: linux-x64-build - path: build/src/ - - build-arm-sf: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-arm-sf:latest - - steps: - - uses: actions/checkout@v3 - - - name: Cross-Compile for ARMv7 - run: | - cmake -B build \ - -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-linux-gnueabihf.cmake \ - -DCMAKE_BUILD_TYPE=Release - - - name: Build - run: cmake --build build --parallel - - - name: Package - run: cpack --config build/CPackConfig.cmake - - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: arm-sf-build - path: build/*.tar.gz - - code-quality: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Run clang-tidy - run: | - cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - run-clang-tidy -p build src/ - - - name: Format Check - run: | - ./tools/format.sh --check -```text - -### 4.2 部署流程 (.github/workflows/deploy.yml) - -```yaml -name: Deploy to Device - -on: - workflow_dispatch: - inputs: - target: - description: 'Target device' - required: true - type: choice - options: - - imx6ull-dev - - rk3568-dev - - rk3588-dev - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Download Build Artifacts - uses: actions/download-artifact@v3 - - - name: Deploy via SSH - uses: appleboy/ssh-action@v0.1.5 - with: - host: ${{ secrets.DEVICE_HOST }} - username: ${{ secrets.DEVICE_USER }} - key: ${{ secrets.DEVICE_SSH_KEY }} - script: | - systemctl stop cfdesktop - tar -xzf cfdesktop.tar.gz -C /opt/ - systemctl start cfdesktop -```yaml - ---- - -## 五、开发环境配置 - -### 5.1 VSCode 配置 (.vscode/settings.json) - -```json -{ - "cmake.configureArgs": [ - "-DBUILD_TESTING=ON" - ], - "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", - "files.associations": { - "*.cmake": "cmake", - "CMakeLists.txt": "cmake" - }, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "C_Cpp.clang_format_style": "file", - "clang-format.style.location": ".clang-format" -} -```text - -### 5.2 推荐扩展 - -- `ms-vscode.cmake-tools` -- `twxs.cmake` -- `xaver.clang-format` -- "xaver.clang-tidy" -- `ms-vscode.cpptools` - -### 5.3 代码格式化配置 (.clang-format) - -```yaml ---- -BasedOnStyle: Google -Language: Cpp -Standard: c++23 - -IndentWidth: 4 -TabWidth: 4 -UseTab: Never -ColumnLimit: 100 - -PointerAlignment: Left -ReferenceAlignment: Left - -SortIncludes: true -IncludeBlocks: Regroup -IncludeCategories: - - Regex: '^= 3.24 | 支持 C++23 和 CMakePresets | -| GCC | >= 12.0 | C++23 特性支持 | -| Qt6 | >= 6.5 | 核心依赖 | -| Ninja | >= 1.10 | 构建后端 | -| Docker | >= 24.0 | CI 容器化 | -| clang-format | >= 16.0 | 代码格式化 | -| clang-tidy | >= 16.0 | 静态分析 | - ---- - -## 九、风险与缓解措施 - -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| Qt6 交叉编译配置复杂 | 延迟 2-3 天 | 使用 Docker 预配置环境 | -| CI 资源不足 | 构建时间长 | 优化依赖缓存,使用矩阵策略 | -| 工具链版本兼容性 | 编译失败 | 固定 Docker 镜像版本 | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| CMake + CMakePresets.json | 跨平台成熟、交叉编译原生支持、IDE 集成广泛 | Meson(生态不够成熟)、QMake(Qt6 已弃用) | +| 三层单向依赖 (base/ui/desktop) | 强制解耦、每层可独立测试、支持嵌入式裁剪 | 扁平结构(耦合严重)、四层以上(过度工程化) | +| 共享库输出 (DLL/SO) | 支持运行时替换模块、减少嵌入式 Flash 占用 | 静态库全链接(编译慢、无法按需裁剪) | +| GitHub Actions 矩阵构建 | 免费额度充足、ARM 交叉编译容器支持好 | Jenkins(维护成本高)、GitLab CI(需自建实例) | +| Git pre-commit hook | 质量门禁前置到本地,减少 CI 失败率 | 仅依赖 CI 检查(反馈周期长) | -## 十、下一步行动 +## 当前状态 -完成 Phase 0 后,立即进入 **Phase 1: 硬件探针与能力分级**。 +已实现 -- 三层目录、CMake 构建体系、GitHub Actions CI、代码格式化配置均已完成。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/01_phase1_hardware_probe.md b/document/design_stage/01_phase1_hardware_probe.md index 75f2b6842..6266ef4d2 100644 --- a/document/design_stage/01_phase1_hardware_probe.md +++ b/document/design_stage/01_phase1_hardware_probe.md @@ -1,1074 +1,30 @@ --- title: "Phase 1: 硬件探针与能力分级详细设计文档" -description: "Phase 1: 硬件探针与能力分级详细设计文档 的详细文档" +description: "硬件检测与三级能力分级:评分算法、策略引擎、用户覆盖机制的设计意图" --- -# Phase 1: 硬件探针与能力分级详细设计文档 +# Phase 1: 硬件探针与能力分级 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 1 - 硬件探针与能力分级 | -| 预计周期 | 2~3 周 | -| 依赖阶段 | Phase 0 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -实现开机自动硬件检测,输出能力档位枚举,作为后续所有模块的行为裁剪依据。 - -### 1.2 具体交付物 -- [ ] `HardwareProbe` 模块及其单元测试 -- [ ] `CapabilityPolicy` 策略引擎 -- [ ] `HWTier` 枚举定义及查询接口 -- [ ] ~~设备配置文件 `/etc/CFDesktop/device.conf`~~ ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API + CFDesktop 自管理目录,不读取系统级路径 -- [ ] Mock 数据集用于单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌─────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (使用 HWTier 查询) │ -├─────────────────────────────────────────────────────────┤ -│ CapabilityPolicy (策略引擎) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ getAnimationPolicy() │ │ -│ │ getRenderingBackend() │ │ -│ │ getVideoDecoderPolicy() │ │ -│ │ getMemoryPolicy() │ │ -│ └────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ HardwareProbe (硬件探针) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ detectCPU() │ │ -│ │ detectGPU() │ │ -│ │ detectMemory() │ │ -│ │ detectNetwork() │ │ -│ │ calculateTier() │ │ -│ └────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Platform Abstraction (平台抽象) │ -│ /proc/cpuinfo /sys/class/ /dev/dri/ evdev │ -└─────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/base/ -├── include/CFDesktop/Base/HardwareProbe/ -│ ├── HWTier.h # 档位枚举定义 -│ ├── HardwareInfo.h # 硬件信息结构体 -│ ├── HardwareProbe.h # 探针主类 -│ ├── CapabilityPolicy.h # 策略引擎 -│ └── DeviceConfig.h # 配置文件读取 -│ -└── src/hardware/ - ├── HardwareProbe.cpp - ├── CapabilityPolicy.cpp - ├── DeviceConfig.cpp - ├── detectors/ # 各检测器实现 - │ ├── CPUDetector.cpp - │ ├── GPUDetector.cpp - │ ├── MemoryDetector.cpp - │ └── NetworkDetector.cpp - └── platform/ # 平台特定实现 - ├── LinuxDetector.cpp - └── WindowsDetector.cpp -```yaml - ---- - -## 三、数据结构定义 - -### 3.1 HWTier 枚举 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HWTier.h` - -```cpp -#pragma once -#include - -namespace CFDesktop::Base { - -/** - * @brief 硬件能力档位枚举 - * - * 定义三个标准档位,每个档位对应不同的硬件配置和性能等级。 - * 后续所有模块根据此枚举决定行为策略。 - */ -enum class HWTier { - /** - * @brief 低端档位 - * - * 典型硬件:IMX6ULL (528MHz Cortex-A7, 无GPU/软GPU) - * - 禁用所有动画 - * - 使用 linuxfb 渲染后端 - * - 软件视频解码 - * - 限制内存使用 < 64MB - */ - Low, - - /** - * @brief 中端档位 - * - * 典型硬件:RK3568 (4xCortex-A55, Mali-G52) - * - 部分动画(淡入淡出、位移) - * - eglfs 可选 - * - 硬件视频解码(H.264/H.265 部分) - * - 内存使用 < 256MB - */ - Mid, - - /** - * @brief 高端档位 - * - * 典型硬件:RK3588 (8xCortex-A76/A55, Mali-G610) - * - 全动画支持(包括缩放、旋转、复杂效果) - * - 强制 eglfs + OpenGL ES 3.2+ - * - 全格式硬件视频解码 - * - 内存使用 < 1GB - */ - High -}; - -/** - * @brief 获取档位字符串描述 - */ -QString tierToString(HWTier tier); - -/** - * @brief 从字符串解析档位 - */ -HWTier tierFromString(const QString& str); - -} // namespace CFDesktop::Base -```text - -### 3.2 HardwareInfo 结构体 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HardwareInfo.h` - -```cpp -#pragma once -#include -#include -#include -#include "HWTier.h" - -namespace CFDesktop::Base { - -/** - * @brief CPU 信息 - */ -struct CPUInfo { - QString model; // CPU 型号名称 - int cores = 0; // 逻辑核心数 - int frequencyMHz = 0; // 主频 (MHz) - QString architecture; // 架构 (armv7l, aarch64, x86_64) - QStringList features; // CPU 特性 (neon, vfpv4, etc.) -}; - -/** - * @brief GPU 信息 - */ -struct GPUInfo { - QString renderer; // 渲染器名称 - QString vendor; // 供应商 - QString version; // 驱动版本 - bool hasHardwareAcceleration = false; // 是否有硬件加速 - QString driverPath; // 驱动设备路径 (/dev/dri/card0) - int maxTextureSize = 0; // 最大纹理尺寸 - QStringList extensions; // OpenGL 扩展列表 -}; - -/** - * @brief 内存信息 - */ -struct MemoryInfo { - quint64 totalBytes = 0; // 总内存 (字节) - quint64 availableBytes = 0; // 可用内存 (字节) - quint64 totalSwap = 0; // 总交换空间 - quint64 freeSwap = 0; // 可用交换空间 - QString lowMemoryBehavior; // 低内存行为 (oom, compress, etc.) -}; - -/** - * @brief 网络接口信息 - */ -struct NetworkInterface { - QString name; // 接口名称 (eth0, wlan0) - bool isUp = false; // 是否启用 - bool isWireless = false; // 是否无线 - QString macAddress; // MAC 地址 - QStringList ipAddresses; // IP 地址列表 -}; - -/** - * @brief 完整硬件信息 - */ -struct HardwareInfo { - HWTier tier = HWTier::Low; // 计算得出的档位 - - CPUInfo cpu; // CPU 信息 - GPUInfo gpu; // GPU 信息 - MemoryInfo memory; // 内存信息 - QList networkInterfaces; // 网络接口 - - QString deviceTreeCompatible; // 设备树 compatible 字符串 - QString boardName; // 板载名称 - - bool isUserOverridden = false; // 是否用户手动覆盖 -}; - -} // namespace CFDesktop::Base -```text - -### 3.3 CapabilityPolicy 配置结构 - -**文件**: `include/CFDesktop/Base/HardwareProbe/CapabilityPolicy.h` - -```cpp -#pragma once -#include "HWTier.h" -#include - -namespace CFDesktop::Base { - -/** - * @brief 动画策略 - */ -struct AnimationPolicy { - bool enabled = false; // 是否启用动画 - int defaultDurationMs = 0; // 默认动画时长 (毫秒) - int maxConcurrentAnimations = 0; // 最大并发动画数 - bool allowComplexEffects = false; // 是否允许复杂效果 (模糊、阴影等) -}; - -/** - * @brief 渲染后端策略 - */ -struct RenderingPolicy { - QString qtPlatform; // Qt 平台插件 (linuxfb, eglfs, xcb) - bool useOpenGL = false; // 是否使用 OpenGL - QString openGLVersion; // OpenGL 版本要求 - bool useVSync = false; // 是否垂直同步 - int maxFPS = 60; // 最大帧率 -}; - -/** - * @brief 视频解码策略 - */ -struct VideoDecoderPolicy { - bool useHardwareDecoder = false; // 是否使用硬件解码 - QStringList supportedCodecs; // 支持的编解码器 - int maxResolution = 0; // 最大分辨率 (宽) - int maxBitrate = 0; // 最大码率 (bps) - bool allowSimultaneousPlayback = false; // 是否允许同时播放多个 -}; - -/** - * @brief 内存策略 - */ -struct MemoryPolicy { - int maxImageCacheBytes = 0; // 图片缓存大小 - int maxFontCacheBytes = 0; // 字体缓存大小 - bool enableTextureCompression = false; // 是否启用纹理压缩 - int maxWindowSurfaces = 0; // 最大窗口表面数 -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 四、类接口设计 - -### 4.1 HardwareProbe 主类 - -**文件**: `include/CFDesktop/Base/HardwareProbe/HardwareProbe.h` - -```cpp -#pragma once -#include "HardwareInfo.h" -#include -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 硬件探针主类 - * - * 负责检测系统硬件能力,输出 HardwareInfo 结构。 - * 单例模式,进程内唯一实例。 - */ -class HardwareProbe : public QObject { - Q_OBJECT - -public: - /** - * @brief 获取单例实例 - */ - static HardwareProbe* instance(); - - /** - * @brief 执行硬件检测 - * @return 检测结果 - * - * 首次调用会执行完整检测,后续调用返回缓存结果。 - * 可通过 forceRedetect() 强制重新检测。 - */ - HardwareInfo probe(); - - /** - * @brief 强制重新检测 - */ - HardwareInfo forceRedetect(); - - /** - * @brief 获取当前硬件档位 - */ - HWTier currentTier() const; - - /** - * @brief 获取完整硬件信息 - */ - const HardwareInfo& hardwareInfo() const; - - /** - * @brief 设置 Mock 数据(仅用于测试) - */ - void setMockData(const HardwareInfo& mockInfo); - - /** - * @brief 检查特定功能是否可用 - */ - bool hasFeature(const QString& feature) const; - -signals: - /** - * @brief 硬件检测完成信号 - */ - void probeCompleted(const HardwareInfo& info); - - /** - * @brief 档位变化信号(热插拔设备等情况) - */ - void tierChanged(HWTier newTier); - -private: - HardwareProbe(QObject* parent = nullptr); - ~HardwareProbe() = default; - - // 禁用拷贝和移动 - HardwareProbe(const HardwareProbe&) = delete; - HardwareProbe& operator=(const HardwareProbe&) = delete; - - // 内部检测方法 - void detectCPU(HardwareInfo& info); - void detectGPU(HardwareInfo& info); - void detectMemory(HardwareInfo& info); - void detectNetwork(HardwareInfo& info); - void calculateTier(HardwareInfo& info); - - // 平台特定实现 - void probeLinux(HardwareInfo& info); - void probeWindows(HardwareInfo& info); - -private: - static HardwareProbe* s_instance; - HardwareInfo m_cachedInfo; - bool m_isProbed = false; - bool m_useMockData = false; - HardwareInfo m_mockInfo; -}; - -} // namespace CFDesktop::Base -```text - -### 4.2 CapabilityPolicy 策略引擎 - -**文件**: `include/CFDesktop/Base/HardwareProbe/CapabilityPolicy.h` - -```cpp -#pragma once -#include "HWTier.h" -#include - -namespace CFDesktop::Base { - -// 前向声明 -struct AnimationPolicy; -struct RenderingPolicy; -struct VideoDecoderPolicy; -struct MemoryPolicy; - -/** - * @brief 能力策略引擎 - * - * 根据 HWTier 提供各模块的能力配置。 - * 单例模式,根据当前硬件档位返回对应策略。 - */ -class CapabilityPolicy : public QObject { - Q_OBJECT - -public: - static CapabilityPolicy* instance(); - - /** - * @brief 获取动画策略 - */ - AnimationPolicy getAnimationPolicy() const; - - /** - * @brief 获取渲染策略 - */ - RenderingPolicy getRenderingPolicy() const; - - /** - * @brief 获取视频解码策略 - */ - VideoDecoderPolicy getVideoDecoderPolicy() const; - - /** - * @brief 获取内存策略 - */ - MemoryPolicy getMemoryPolicy() const; - - /** - * @brief 获取当前档位 - */ - HWTier currentTier() const; - - /** - * @brief 强制设置档位(用于测试或用户覆盖) - */ - void overrideTier(HWTier tier); - -signals: - void policyChanged(HWTier newTier); - -private: - CapabilityPolicy(QObject* parent = nullptr); - ~CapabilityPolicy() = default; - - // 档位特定策略配置 - AnimationPolicy getAnimationPolicyForLow() const; - AnimationPolicy getAnimationPolicyForMid() const; - AnimationPolicy getAnimationPolicyForHigh() const; - - RenderingPolicy getRenderingPolicyForLow() const; - RenderingPolicy getRenderingPolicyForMid() const; - RenderingPolicy getRenderingPolicyForHigh() const; - - VideoDecoderPolicy getVideoPolicyForLow() const; - VideoDecoderPolicy getVideoPolicyForMid() const; - VideoDecoderPolicy getVideoPolicyForHigh() const; - - MemoryPolicy getMemoryPolicyForLow() const; - MemoryPolicy getMemoryPolicyForMid() const; - MemoryPolicy getMemoryPolicyForHigh() const; - -private: - static CapabilityPolicy* s_instance; - HWTier m_currentTier = HWTier::Low; - bool m_isOverridden = false; -}; - -} // namespace CFDesktop::Base -```text - -### 4.3 DeviceConfig 配置文件 - -**文件**: `include/CFDesktop/Base/HardwareProbe/DeviceConfig.h` - -```cpp -#pragma once -#include "HWTier.h" -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 设备配置文件读取器 - * - * ⚠️ **过时**: 实际实现使用 `setDeviceConfigOverride()` API,配置在 CFDesktop 自管理目录内。 - * 以下 `/etc/CFDesktop/` 路径仅为早期设计参考。 - * - * 读取 /etc/CFDesktop/device.conf,允许用户手动覆盖硬件检测。 - * - * 配置文件格式: - * [Device] - * Tier=auto|low|mid|high - * CustomScript=/path/to/detection/script - * - * [Overrides] - * EnableAnimations=true|false - * ForceOpenGL=true|false - */ -class DeviceConfig { -public: - struct Config { - bool tierAutoDetect = true; - HWTier forcedTier = HWTier::Low; - QString customScriptPath; - QVariantMap overrides; - }; - - /** - * @brief 加载配置文件 - */ - static Config load(const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时:实际使用应用内目录 - - /** - * @brief 保存配置文件 - */ - static bool save(const Config& config, - const QString& path = "/etc/CFDesktop/device.conf"); // ⚠️ 过时 - - /** - * @brief 执行自定义检测脚本 - * - * 脚本输出格式: JSON - * { - * "tier": "low|mid|high", - * "properties": {...} - * } - */ - static QVariantMap executeCustomScript(const QString& scriptPath); -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、检测逻辑详细设计 - -### 5.1 CPU 检测 - -#### 5.1.1 Linux 平台 - -**检测文件**: `src/hardware/detectors/CPUDetector.cpp` - -```cpp -CPUInfo CPUDetector::detectCPU() { - CPUInfo info; +CFDesktop 的目标设备跨度极大——从 528MHz 单核 Cortex-A7 (IMX6ULL) 到 8 核 Cortex-A76 (RK3588),性能差距超过 20 倍。如果用一套渲染和动画策略覆盖所有设备,要么在低端设备上卡顿,要么在高端设备上浪费算力。因此我们在系统启动时执行一次硬件探针,输出三级档位(Low/Mid/High),后续所有模块(主题引擎、动画管理器、渲染后端)根据档位自动裁剪行为。 - // 1. 读取 /proc/cpuinfo - QFile cpuInfo("/proc/cpuinfo"); - if (cpuInfo.open(QIODevice::ReadOnly)) { - QTextStream in(&cpuInfo); - QSet uniqueProcessors; +选择三级而非更细的分级(如五级或连续分数),是因为三级足以覆盖我们的目标硬件矩阵,且每级对应一组明确的能力策略(禁用动画/基础动画/全动画),避免了策略配置的指数膨胀。评分采用加权累加制:CPU 核数和频率、GPU 硬件加速、内存容量各占一定权重,阈值设为 60/120 分两档切分。 - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.startsWith("Hardware")) { - info.model = line.section(':', 1).trimmed(); - } else if (line.startsWith("Processor")) { - uniqueProcessors.insert(line); - } else if (line.startsWith("CPU implementer")) { - info.architecture = detectArchitecture(line); - } else if (line.startsWith("Features")) { - info.features = line.section(':', 1).trimmed().split(' '); - } else if (line.startsWith("BogoMIPS")) { - info.frequencyMHz = static_cast( - line.section(':', 1).trimmed().toDouble() * 2); - } - } - info.cores = uniqueProcessors.size(); - } +检测方法按平台区分:Linux 通过 `/proc/cpuinfo`、`/proc/meminfo`、`/dev/dri/`、`/sys/class/net/` 等 sysfs 路径直接读取;Windows 通过 WMI 查询。GPU 检测额外尝试创建 OpenGL 上下文获取驱动信息和扩展列表。所有检测结果缓存在 `HardwareInfo` 结构体中,避免重复检测。 - // 2. 通过 uname 确认架构 - QProcess uname; - uname.start("uname", QStringList() << "-m"); - if (uname.waitForStarted() && uname.waitForFinished()) { - info.architecture = uname.readAllStandardOutput().trimmed(); - } +用户或系统集成商可通过 `setDeviceConfigOverride()` API 强制覆盖自动检测结果,也可提供自定义检测脚本(输出 JSON)用于扩展硬件识别。这保证了在特殊硬件(如定制板卡)上的灵活性。 - // 3. 读取设备树 (如果存在) - QFile compatible("/sys/firmware/devicetree/base/compatible"); - if (compatible.open(QIODevice::ReadOnly)) { - QByteArray data = compatible.readAll(); - // 设备树字符串以 null 结尾,可能有多项 - QList compatList = data.split('\0'); - if (!compatList.isEmpty() && !compatList.first().isEmpty()) { - info.model = compatList.first(); - } - } +## 关键决策 - return info; -} - -QString CPUDetector::detectArchitecture(const QString& cpuInfoLine) { - // CPU implementer 值映射 - QHash implementerMap = { - {"0x41", "ARM"}, - {"0x42", "Broadcom"}, - {"0x43", "Cavium"}, - {"0x44", "DEC"}, - {"0x49", "Infineon"}, - {"0x4d", "Motorola/Freescale"}, - {"0x4e", "NVIDIA"}, - {"0x50", "APM"}, - {"0x51", "Qualcomm"}, - {"0x56", "Marvell"}, - {"0x69", "Intel"}, - }; - - QString implementer = /* 从 cpuinfo 提取 */; - return implementerMap.value(implementer, "Unknown"); -} -```text - -#### 5.1.2 Windows 平台 - -```cpp -CPUInfo CPUDetector::detectCPUWindows() { - CPUInfo info; - - // 使用 WMI 查询 - QProcess wmic; - wmic.start("wmic", QStringList() - << "cpu" << "get" << "Name,NumberOfCores,MaxClockSpeed"); - // 解析输出... - - return info; -} -```text - -### 5.2 GPU 检测 - -```cpp -GPUInfo GPUDetector::detectGPU() { - GPUInfo info; - - // 1. 检查 DRM 设备 - QDir dri("/dev/dri"); - if (dri.exists()) { - QStringList devices = dri.entryList(QStringList() << "card*", QDir::Files); - if (!devices.isEmpty()) { - info.hasHardwareAcceleration = true; - info.driverPath = "/dev/dri/" + devices.first(); - } - } - - // 2. 尝试创建 OpenGL 上下文 - QOffscreenSurface surface; - QOpenGLContext context; - if (context.create()) { - context.makeCurrent(&surface); - - auto* functions = context.functions(); - info.vendor = reinterpret_cast( - functions->glGetString(GL_VENDOR)); - info.renderer = reinterpret_cast( - functions->glGetString(GL_RENDERER)); - info.version = reinterpret_cast( - functions->glGetString(GL_VERSION)); - - // 获取扩展列表 - auto* extraFunctions = context.extraFunctions(); - GLint numExtensions = 0; - functions->glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions); - for (int i = 0; i < numExtensions; ++i) { - const char* ext = reinterpret_cast( - functions->glGetStringi(GL_EXTENSIONS, i)); - info.extensions.append(ext); - } - - context.doneCurrent(); - } - - // 3. 检查特定 SoC 的 GPU 型号 - detectSoCGPU(info); - - return info; -} -```text - -### 5.3 内存检测 - -```cpp -MemoryInfo MemoryDetector::detectMemory() { - MemoryInfo info; - - // Linux: 读取 /proc/meminfo - QFile memInfo("/proc/meminfo"); - if (memInfo.open(QIODevice::ReadOnly)) { - QTextStream in(&memInfo); - QHash memData; - - while (!in.atEnd()) { - QString line = in.readLine(); - QString key = line.section(':', 0, 0); - QString value = line.section(':', 1).section(' ', 0, 0); - memData[key] = value.toULongLong() * 1024; // kB to bytes - } - - info.totalBytes = memData["MemTotal"]; - info.availableBytes = memData["MemAvailable"]; - info.totalSwap = memData["SwapTotal"]; - info.freeSwap = memData["SwapFree"]; - } - - return info; -} -```text - -### 5.4 网络检测 - -```cpp -QList NetworkDetector::detectNetwork() { - QList interfaces; - - // 读取 /sys/class/net - QDir netDir("/sys/class/net"); - QStringList devices = netDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - - for (const QString& device : devices) { - NetworkInterface iface; - iface.name = device; - - // 检查是否启用 - QFile operstate(QString("/sys/class/net/%1/operstate").arg(device)); - if (operstate.open(QIODevice::ReadOnly)) { - iface.isUp = operstate.readAll().trimmed() == "up"; - } - - // 检查是否无线 - QDir wireless("/sys/class/net/" + device + "/wireless"); - iface.isWireless = wireless.exists(); - - // 读取 MAC 地址 - QFile address(QString("/sys/class/net/%1/address").arg(device)); - if (address.open(QIODevice::ReadOnly)) { - iface.macAddress = address.readAll().trimmed(); - } - - // 通过 Socket ioctl 获取 IP 地址 - iface.ipAddresses = getIPAddresses(device); - - interfaces.append(iface); - } - - return interfaces; -} -```text - -### 5.5 档位计算逻辑 - -```cpp -void HardwareProbe::calculateTier(HardwareInfo& info) { - // 默认为 Low 档 - HWTier calculatedTier = HWTier::Low; - int score = 0; - - // CPU 评分 - score += std::min(info.cpu.cores, 8) * 10; // 最多 80 分 - if (info.cpu.frequencyMHz > 1000) score += 20; - if (info.cpu.features.contains("neon")) score += 10; - - // GPU 评分 - if (info.gpu.hasHardwareAcceleration) score += 50; - if (!info.gpu.driverPath.isEmpty()) score += 20; - - // 内存评分 - int memoryMB = info.memory.totalBytes / (1024 * 1024); - if (memoryMB >= 512) score += 20; - if (memoryMB >= 1024) score += 20; - if (memoryMB >= 2048) score += 10; - - // 根据评分决定档位 - // Low: 0-60, Mid: 61-120, High: 121+ - if (score >= 121) { - calculatedTier = HWTier::High; - } else if (score >= 61) { - calculatedTier = HWTier::Mid; - } else { - calculatedTier = HWTier::Low; - } - - info.tier = calculatedTier; -} -```yaml - ---- - -## 六、设备配置文件格式 - -> ⚠️ **过时说明**: 以下 `/etc/CFDesktop/` 路径为早期设计。实际实现中,CFDesktop 不读取系统级路径,设备配置通过 `setDeviceConfigOverride()` API 或 ConfigStore 应用内目录管理。 - -### 6.1 配置文件模板 - -**文件**: `configs/device.conf.template` - -```ini -# CFDesktop 设备配置文件 -# 部署位置: CFDesktop 自管理目录内 (非 /etc/) - -[Device] -# 档位设置: auto(自动检测) | low | mid | high -Tier=auto - -# 自定义检测脚本路径(可选) -# 脚本应输出 JSON 格式到 stdout -CustomScript=/opt/cfdesktop/detect-hardware.sh - -# 板载名称(可选,用于日志) -BoardName=Generic-Board - -[Overrides] -# 强制覆盖特定能力(可选) - -# 动画设置 -EnableAnimations=true -AnimationDuration=200 - -# 渲染后端: auto | linuxfb | eglfs | xcb -RenderingBackend=auto - -# OpenGL 设置 -ForceOpenGL=false - -# 视频解码: auto | software | hardware -VideoDecoder=auto - -[Logging] -# 硬件检测日志级别 -LogLevel=Info -```text - -### 6.2 自定义检测脚本示例 - -```bash -#!/bin/bash -# /opt/cfdesktop/detect-hardware.sh - -# 检测是否连接了特定扩展硬件 -if [ -e /sys/bus/i2c/devices/0-0050 ]; then - # 检测到扩展显示屏,提升档位 - echo '{"tier": "mid", "properties": {"externalDisplay": true}}' -else - echo '{"tier": "low"}' -fi -```yaml - ---- - -## 七、单元测试设计 - -### 7.1 Mock 数据目录结构 - -```text -tests/mock/ -└── proc/ - ├── cpuinfo_imx6ull # IMX6ULL CPU 信息 - ├── cpuinfo_rk3568 # RK3568 CPU 信息 - ├── cpuinfo_rk3588 # RK3588 CPU 信息 - ├── meminfo_512mb # 512MB 内存 - ├── meminfo_1gb # 1GB 内存 - ├── meminfo_4gb # 4GB 内存 - └── devices/ # 模拟设备文件 - └── dri/ - └── card0 # 模拟 GPU 设备 -```text - -### 7.2 测试用例清单 - -**文件**: `tests/unit/base/hardware/test_hardware_probe.cpp` - -```cpp -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - // ========== CPU 检测测试 ========== - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - void testDetectCPU_X86_64(); - - // ========== GPU 检测测试 ========== - void testDetectGPU_WithDRM(); - void testDetectGPU_NoDRM(); - void testDetectGPU_OpenGLContext(); - - // ========== 内存检测测试 ========== - void testDetectMemory_512MB(); - void testDetectMemory_1GB(); - void testDetectMemory_4GB(); - - // ========== 档位计算测试 ========== - void testCalculateTier_IMX6ULL_returnsLow(); - void testCalculateTier_RK3568_returnsMid(); - void testCalculateTier_RK3588_returnsHigh(); - - // ========== 配置文件测试 ========== - void testDeviceConfig_LoadDefault(); - void testDeviceConfig_OverrideTier(); - void testDeviceConfig_CustomScript(); - - // ========== 策略引擎测试 ========== - void testCapabilityPolicy_LowTier(); - void testCapabilityPolicy_MidTier(); - void testCapabilityPolicy_HighTier(); - void testCapabilityPolicy_AnimationDisabledOnLow(); - - // ========== 边界情况 ========== - void testEmptyProcFiles(); - void testMalformedConfig(); - void testTierOverride(); -}; -```text - -### 7.3 示例测试用例 - -```cpp -void TestHardwareProbe::testDetectCPU_IMX6ULL() { - // 1. 设置 Mock 环境变量 - qputenv("CFDESKTOP_MOCK_PROC", "/path/to/mock/proc/cpuinfo_imx6ull"); - - // 2. 创建探针实例 - HardwareProbe probe; - HardwareInfo info = probe.probe(); - - // 3. 验证结果 - QCOMPARE(info.cpu.model, "Freescale i.MX6 UltraLite"); - QCOMPARE(info.cpu.cores, 1); - QVERIFY(info.cpu.features.contains("neon")); - QCOMPARE(info.cpu.architecture, "armv7l"); - - // 4. 验证档位 - QCOMPARE(info.tier, HWTier::Low); -} -```bash - ---- - -## 八、详细任务清单 - -### 8.1 Week 1: 基础检测器实现 - -#### Day 1-2: CPU 检测器 -- [ ] 创建 `CPUDetector` 类框架 -- [ ] 实现 `/proc/cpuinfo` 解析 -- [ ] 实现设备树 compatible 读取 -- [ ] 实现 `uname` 架构检测 -- [ ] 编写 CPU 检测单元测试 - -#### Day 3: GPU 检测器 -- [ ] 创建 `GPUDetector` 类框架 -- [ ] 实现 DRM 设备检测 -- [ ] 实现 OpenGL 上下文创建 -- [ ] 实现 GPU 能力查询 -- [ ] 编写 GPU 检测单元测试 - -#### Day 4: 内存与网络检测器 -- [ ] 实现 `/proc/meminfo` 解析 -- [ ] 实现网络接口枚举 -- [ ] 实现 IP 地址获取 -- [ ] 编写相应单元测试 - -#### Day 5: 档位计算逻辑 -- [ ] 实现评分算法 -- [ ] 定义档位阈值 -- [ ] 实现 `calculateTier()` 方法 -- [ ] 验证各板卡档位判定 - -### 8.2 Week 2: 策略引擎与配置 - -#### Day 1-2: CapabilityPolicy 实现 -- [ ] 定义各策略结构体 -- [ ] 实现档位特定策略配置 -- [ ] 实现 `getAnimationPolicy()` -- [ ] 实现 `getRenderingPolicy()` -- [ ] 实现 `getVideoDecoderPolicy()` -- [ ] 实现 `getMemoryPolicy()` - -#### Day 3: DeviceConfig 实现 -- [ ] 实现配置文件解析 -- [ ] 实现配置文件写入 -- [ ] 实现自定义脚本执行 -- [ ] 编写配置测试 - -#### Day 4: 集成测试 -- [ ] 创建端到端测试用例 -- [ ] 测试 IMX6ULL 真机 -- [ ] 测试 RK3568 真机 -- [ ] 验证自动档位判定 - -#### Day 5: 文档与优化 -- [ ] 编写 API 文档 -- [ ] 优化检测性能 -- [ ] 添加性能日志 -- [ ] Code Review - -### 8.3 Week 3: 完善与验证 - -#### Day 1-2: 跨平台支持 -- [ ] 实现 Windows 平台检测 -- [ ] 实现模拟器平台适配 -- [ ] 编写平台特定测试 - -#### Day 3-4: 真机验证 -- [ ] 在 IMX6ULL 上完整测试 -- [ ] 在 RK3568 上完整测试 -- [ ] 在 RK3588 上完整测试 -- [ ] 收集性能数据 - -#### Day 5: 发布准备 -- [ ] 最终测试 -- [ ] 更新文档 -- [ ] 准备演示 -- [ ] 合并主分支 - ---- - -## 九、验收标准 - -### 9.1 功能验收 -- [ ] 在 IMX6ULL 上正确识别为 Low 档 -- [ ] 在 RK3568 上正确识别为 Mid 档 -- [ ] 在 RK3588 上正确识别为 High 档 -- [ ] 所有单元测试通过(覆盖率 > 90%) -- [ ] 配置文件覆盖功能正常 -- [ ] 自定义脚本执行正常 - -### 9.2 性能验收 -- [ ] 硬件检测耗时 < 500ms -- [ ] 内存占用 < 5MB -- [ ] 不影响系统启动时间 - -### 9.3 代码质量 -- [ ] 符合代码规范 -- [ ] 通过 clang-tidy 检查 -- [ ] API 文档完整 -- [ ] 代码审查通过 - ---- - -## 十、已知问题与风险 - -| 问题 | 风险 | 缓解措施 | -|------|------|----------| -| 某些板卡没有设备树 | 档位误判 | 提供 CPU 特性回退检测 | -| GPU 驱动不稳定 | 检测失败 | 添加异常处理,设置超时 | -| 交叉编译测试困难 | 覆盖不足 | 使用 QEMU 用户模式模拟 | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三级档位 (Low/Mid/High) | 覆盖目标硬件矩阵、策略配置简洁、避免过度细分 | 五级或连续分数(策略组合爆炸)、两级(粒度不够) | +| 加权评分算法 (CPU+GPU+Memory) | 各维度独立评分、阈值明确、易于调试 | 机器学习模型(过重、训练数据不足)、纯特征匹配(不通用) | +| 运行时检测 + 缓存 | 一次检测、全局复用、性能开销可控 | 编译时硬编码(无法适配新硬件)、每次查询都检测(性能浪费) | +| 策略引擎按档位返回策略结构体 | 上层模块只需读策略、不关心检测细节 | 上层模块自行解读硬件信息(逻辑分散、重复代码) | +| `setDeviceConfigOverride()` API 覆盖 | 灵活、不依赖系统路径、适配嵌入式部署 | 读取 `/etc/CFDesktop/device.conf`(需要 root 权限、路径不可移植) | -## 十一、下一步行动 +## 当前状态 -完成 Phase 1 后,进入 **Phase 2: Base 库核心**。 +已实现 -- CPU/GPU/Memory/Network 检测器、三级评分算法、策略引擎、用户覆盖 API 均已完成。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/02_phase2_base_library.md b/document/design_stage/02_phase2_base_library.md index 117f80057..546390c3d 100644 --- a/document/design_stage/02_phase2_base_library.md +++ b/document/design_stage/02_phase2_base_library.md @@ -1,1308 +1,32 @@ --- title: "Phase 2: Base 库核心详细设计文档" -description: "Phase 2: Base 库核心详细设计文档 的详细文档" +description: "主题引擎五层流水线、DPI 适配、四层 ConfigStore、异步日志系统的设计意图" --- -# Phase 2: Base 库核心详细设计文档 +# Phase 2: Base 库核心 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 2 - Base 库核心 | -| 预计周期 | 3~4 周 | -| 依赖阶段 | Phase 0, Phase 1 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立所有上层模块依赖的基础设施,包括主题引擎、动画管理、分辨率适配、配置中心和日志系统。 - -### 1.2 具体交付物 -- [x] `ThemeEngine` 主题引擎模块 -- [x] `AnimationManager` 动画管理器 -- [ ] `DPIManager` 分辨率适配管理器 -- [x] `ConfigStore` 配置中心 -- [x] `Logger` 日志系统 -- [x] 各模块单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体依赖关系 - -```text -┌──────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (Shell / Third-party Apps) │ -├──────────────────────────────────────────────────────────┤ -│ SDK Layer │ -├──────────────────────────────────────────────────────────┤ -│ Base Library │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ThemeEngine │ │AnimationMgr │ │ DPIManager │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ ConfigStore │ │ Logger │ │HardwareProbe │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -├──────────────────────────────────────────────────────────┤ -│ Qt6 Framework │ -│ Core / Gui / Widgets / Multimedia / Network │ -└──────────────────────────────────────────────────────────┘ -```text - -### 2.2 Base 库目录结构 - -```text -src/base/ -├── include/CFDesktop/Base/ -│ ├── ThemeEngine/ -│ │ ├── ThemeEngine.h # 主题引擎主类 -│ │ ├── Theme.h # 主题数据结构 -│ │ ├── ThemeLoader.h # 主题加载器 -│ │ └── ThemeVariables.h # 主题变量系统 -│ │ -│ ├── AnimationManager/ -│ │ ├── AnimationManager.h # 动画管理器 -│ │ ├── Animation.h # 动画基类 -│ │ ├── PropertyAnimation.h # 属性动画 -│ │ ├── GroupAnimation.h # 组动画 -│ │ └── AnimationPolicy.h # 动画策略(来自 Phase 1) -│ │ -│ ├── DPIManager/ -│ │ ├── DPIManager.h # DPI 管理器 -│ │ ├── DisplayInfo.h # 显示信息 -│ │ └── DPIConverter.h # 单位转换工具 -│ │ -│ ├── ConfigStore/ -│ │ ├── ConfigStore.h # 配置存储主类 -│ │ ├── ConfigNode.h # 配置节点 -│ │ └── ConfigWatcher.h # 配置监视器 -│ │ -│ └── Logger/ -│ ├── Logger.h # 日志主类 -│ ├── LogMessage.h # 日志消息 -│ ├── LogSink.h # 日志输出抽象 -│ ├── FileSink.h # 文件输出 -│ ├── ConsoleSink.h # 控制台输出 -│ └── NetworkSink.h # 网络输出 -│ -└── src/ - ├── theme/ - │ ├── ThemeEngine.cpp - │ ├── ThemeLoader.cpp - │ ├── QSSProcessor.cpp # QSS 处理器 - │ └── VariableResolver.cpp # 变量解析器 - │ - ├── animation/ - │ ├── AnimationManager.cpp - │ ├── PropertyAnimation.cpp - │ └── GroupAnimation.cpp - │ - ├── dpi/ - │ ├── DPIManager.cpp - │ └── DPIConverter.cpp - │ - ├── config/ - │ ├── ConfigStore.cpp - │ └── ConfigWatcher.cpp - │ - └── logging/ - ├── Logger.cpp - ├── FileSink.cpp - ├── ConsoleSink.cpp - └── NetworkSink.cpp -```yaml - ---- - -## 三、主题引擎 (ThemeEngine) - -### 3.1 主题包结构 - -```text -assets/themes/ -└── default/ - ├── theme.json # 主题元数据 - ├── variables/ - │ ├── colors.json # 颜色变量 - │ ├── sizes.json # 尺寸变量 - │ └── fonts.json # 字体变量 - ├── styles/ - │ ├── base.qss # 基础样式 - │ ├── widgets.qss # 控件样式 - │ └── animations.qss # 动画样式 - └── icons/ - ├── actions/ # 动作图标 - ├── status/ # 状态图标 - └── devices/ # 设备图标 -```text - -### 3.2 主题元数据格式 - -**文件**: `assets/themes/default/theme.json` - -```json -{ - "meta": { - "name": "Default Light", - "version": "1.0.0", - "author": "CFDesktop Team", - "description": "Default light theme", - "hwTier": "any" - }, - "inherit": null, - "settings": { - "windowOpacity": 0.95, - "enableBlur": true, - "enableShadows": true, - "animationPolicy": "full" - } -} -```text - -### 3.3 颜色变量定义 - -**文件**: `assets/themes/default/variables/colors.json` - -```json -{ - "primary": "#2196F3", - "primaryDark": "#1976D2", - "primaryLight": "#BBDEFB", - "secondary": "#FF9800", - "accent": "#FF4081", - "background": "#FFFFFF", - "surface": "#F5F5F5", - "error": "#F44336", - "success": "#4CAF50", - "warning": "#FFC107", - "info": "#2196F3", - "text": { - "primary": "#212121", - "secondary": "#757575", - "disabled": "#BDBDBD", - "inverse": "#FFFFFF" - }, - "divider": "#E0E0E0", - "shadow": "rgba(0, 0, 0, 0.12)" -} -```text - -### 3.4 QSS 模板语法 - -```qss -/* assets/themes/default/styles/base.qss */ -QWidget { - background-color: @background; - color: @text.primary; -} - -QPushButton { - background-color: @primary; - color: @text.inverse; - border: none; - border-radius: @radius.medium; - padding: @padding.normal; - font-size: @font.size.medium; -} - -QPushButton:hover { - background-color: @primaryLight; -} - -QPushButton:pressed { - background-color: @primaryDark; -} - -QPushButton:disabled { - background-color: @divider; - color: @text.disabled; -} -```text - -### 3.5 ThemeEngine 类接口 - -**文件**: `include/CFDesktop/Base/ThemeEngine/ThemeEngine.h` - -```cpp -#pragma once -#include -#include -#include -#include -#include -#include "Theme.h" - -namespace CFDesktop::Base { - -/** - * @brief 主题引擎主类 - * - * 负责加载、切换和管理主题,提供变量查询接口。 - * 单例模式,进程内唯一实例。 - */ -class ThemeEngine : public QObject { - Q_OBJECT - -public: - static ThemeEngine* instance(); - - /** - * @brief 加载主题 - * @param themePath 主题路径 - * @return 是否成功 - */ - bool loadTheme(const QString& themePath); - - /** - * @brief 切换主题 - * @param themeName 主题名称 - */ - void switchTheme(const QString& themeName); - - /** - * @brief 获取当前主题 - */ - Theme currentTheme() const; - - /** - * @brief 获取颜色变量 - * @param key 变量名 (支持点分隔路径: "text.primary") - */ - QColor color(const QString& key) const; - - /** - * @brief 获取尺寸变量 - * @param key 变量名 - */ - int size(const QString& key) const; - - /** - * @brief 获取字体变量 - * @param key 变量名 - */ - QFont font(const QString& key) const; - - /** - * @brief 获取任意变量 - */ - QVariant variable(const QString& key) const; - - /** - * @brief 应用主题到应用 - * @param app 目标应用 - */ - void applyToApplication(QApplication* app); - - /** - * @brief 处理 QSS 中的变量引用 - * @param qss 原始 QSS - * @return 处理后的 QSS - */ - QString processQSS(const QString& qss) const; - - /** - * @brief 获取当前主题样式表 - */ - QString styleSheet() const; - - /** - * @brief 重新加载主题(热更新) - */ - void reload(); - -signals: - /** - * @brief 主题变化信号 - */ - void themeChanged(const Theme& theme); - - /** - * @brief 颜色变量变化信号 - */ - void colorChanged(const QString& key, const QColor& color); - -private: - ThemeEngine(QObject* parent = nullptr); - ~ThemeEngine() = default; - - void loadVariables(const QString& path); - void loadStyles(const QString& path); - void resolveThemeInheritance(Theme& theme); - QString resolveVariables(const QString& input) const; - -private: - static ThemeEngine* s_instance; - Theme m_currentTheme; - QMap m_colors; - QMap m_sizes; - QMap m_fonts; - QMap m_styleSheets; - QString m_themePath; -}; - -} // namespace CFDesktop::Base -```bash - -### 3.6 主题降级策略 - -根据 HWTier 自动应用不同效果: - -| 特性 | Low 档 | Mid 档 | High 档 | -|------|--------|--------|---------| -| 阴影效果 | 禁用 | 简单阴影 | 完整阴影 | -| 圆角 | 小圆角 (2px) | 中圆角 (4px) | 大圆角 (8px) | -| 模糊背景 | 禁用 | 半透明模糊 | 全模糊 | -| 渐变 | 禁用 | 简单渐变 | 复杂渐变 | -| 动画 | 禁用 | 基础动画 | 完整动画 | - ---- - -## 四、动画管理器 (AnimationManager) - -### 4.1 AnimationManager 类接口 - -**文件**: `include/CFDesktop/Base/AnimationManager/AnimationManager.h` - -```cpp -#pragma once -#include -#include -#include -#include -#include "Animation.h" - -namespace CFDesktop::Base { +Phase 2 是整个框架的基础设施层,为上层 UI 和桌面环境提供主题、动画、分辨率适配、配置和日志五大核心服务。这些服务必须在设计阶段就考虑嵌入式设备的资源约束——任何模块都不能在低端设备上成为性能瓶颈。 -/** - * @brief 动画管理器 - * - * 统一管理所有动画,根据 HWTier 自动调整动画参数。 - * Low 档位时所有动画时长归零,实现透明降级。 - */ -class AnimationManager : public QObject { - Q_OBJECT +**主题引擎**最终演化为五层流水线架构(Math & Utility -> Theme Engine -> Animation Engine -> Material Behavior -> Widget Adapter)。这一分层并非预先设计,而是在实现过程中发现:颜色运算(CFColor、GeometryHelper)与主题切换逻辑(ThemeManager、token system)职责不同,动画策略(CFMaterialAnimationFactory)与 Material Design 状态机行为(StateMachine、RippleHelper)也应该分离。五层流水线让每一层可以独立测试和替换,Low 档设备可以跳过高层(Layer 4/5)直接使用简化渲染。 -public: - static AnimationManager* instance(); - - /** - * @brief 创建属性动画 - * @param target 目标对象 - * @param propertyName 属性名 - * @param startValue 起始值 - * @param endValue 结束值 - * @param duration 动画时长 (毫秒),Low 档会自动归零 - * @return 动画指针 - */ - QPropertyAnimation* createPropertyAnimation( - QObject* target, - const QByteArray& propertyName, - const QVariant& startValue, - const QVariant& endValue, - int duration = 300 - ); - - /** - * @brief 创建淡入动画 - */ - QPropertyAnimation* createFadeIn( - QWidget* widget, - int duration = 300 - ); - - /** - * @brief 创建淡出动画 - */ - QPropertyAnimation* createFadeOut( - QWidget* widget, - int duration = 300 - ); - - /** - * @brief 创建滑入动画 - */ - QPropertyAnimation* createSlideIn( - QWidget* widget, - Qt::Direction direction, - int distance = 100, - int duration = 300 - ); - - /** - * @brief 创建缩放动画 - */ - QPropertyAnimation* createScale( - QWidget* widget, - qreal fromScale, - qreal toScale, - int duration = 300 - ); - - /** - * @brief 创建旋转动画 - */ - QPropertyAnimation* createRotation( - QWidget* widget, - qreal fromAngle, - qreal toAngle, - int duration = 300 - ); - - /** - * @brief 创建并行动画组 - */ - QParallelAnimationGroup* createParallelGroup( - const QList& animations - ); - - /** - * @brief 创建串行动画组 - */ - QSequentialAnimationGroup* createSequentialGroup( - const QList& animations - ); - - /** - * @brief 启动动画 - */ - void start(QAbstractAnimation* animation); - - /** - * @brief 停止动画 - */ - void stop(QAbstractAnimation* animation); - - /** - * @brief 停止所有动画 - */ - void stopAll(); - - /** - * @brief 调整动画时长(根据 HWTier) - * @param originalDuration 原始时长 - * @return 调整后的时长 - */ - int adjustDuration(int originalDuration) const; - - /** - * @brief 是否启用动画 - */ - bool isAnimationEnabled() const; - -signals: - /** - * @brief 动画策略变化信号 - */ - void animationPolicyChanged(const AnimationPolicy& policy); - -private: - AnimationManager(QObject* parent = nullptr); - ~AnimationManager() = default; - - void updatePolicy(); - QPropertyAnimation* createBaseAnimation( - QObject* target, - const QByteArray& propertyName - ); - -private: - static AnimationManager* s_instance; - AnimationPolicy m_policy; - QList> m_runningAnimations; -}; - -} // namespace CFDesktop::Base -```text - -### 4.2 预定义动画类型 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 预定义动画类型 - */ -enum class AnimationType { - None, // 无动画 - Fade, // 淡入淡出 - Slide, // 滑动 - Scale, // 缩放 - Rotate, // 旋转 - Bounce, // 弹跳 - Elastic, // 弹性 - Complex // 复杂组合 -}; - -/** - * @brief 缓动曲线类型 - */ -enum class EasingType { - Linear, - InQuad, OutQuad, InOutQuad, - InCubic, OutCubic, InOutCubic, - InQuart, OutQuart, InOutQuart, - InQuint, OutQuint, InOutQuint, - InBack, OutBack, InOutBack, - InBounce, OutBounce, InOutBounce -}; - -/** - * @brief 获取 Qt 缓动曲线 - */ -QEasingCurve easingCurve(EasingType type); - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、DPI 管理器 (DPIManager) - -### 5.1 DPIManager 类接口 - -**文件**: `include/CFDesktop/Base/DPIManager/DPIManager.h` - -```cpp -#pragma once -#include -#include -#include "DisplayInfo.h" - -namespace CFDesktop::Base { - -/** - * @brief DPI 管理器 - * - * 提供分辨率无关的尺寸单位,支持多屏幕和热插拔。 - */ -class DPIManager : public QObject { - Q_OBJECT - -public: - static DPIManager* instance(); - - /** - * @brief 密度无关像素 (dp) 转换为物理像素 - * - * 类 Android 的 dp 单位,在 160dpi 屏幕上 1dp = 1px - */ - int dp(int value) const; - - /** - * @brief 缩放无关像素 (sp) 转换为物理像素 - * - * 用于字体大小,会跟随用户字体缩放设置 - */ - int sp(int value) const; - - /** - * @brief 物理像素转换为 dp - */ - int pxToDp(int pixels) const; - - /** - * @brief 获取屏幕 DPI - */ - int screenDPI() const; - - /** - * @brief 获取设备像素比 - */ - qreal devicePixelRatio() const; - - /** - * @brief 获取屏幕尺寸 (英寸) - */ - QSizeF screenSizeInches() const; - - /** - * @brief 获取屏幕逻辑分辨率 - */ - QSize logicalSize() const; - - /** - * @brief 获取屏幕物理分辨率 - */ - QSize physicalSize() const; - - /** - * @brief 是否为高 DPI 屏幕 - */ - bool isHighDPI() const; - - /** - * @brief 注入屏幕参数(用于模拟器) - */ - void injectScreenParameters( - int dpi, - qreal devicePixelRatio, - const QSize& size - ); - - /** - * @brief 清除注入的参数 - */ - void clearInjectedParameters(); - -signals: - /** - * @brief DPI 变化信号 - */ - void dpiChanged(int oldDpi, int newDpi); - - /** - * @brief 屏幕变化信号 - */ - void screenChanged(const QRect& geometry); - -private: - DPIManager(QObject* parent = nullptr); - ~DPIManager() = default; - - void detectScreenParameters(); - void updateDpi(); - -private: - static DPIManager* s_instance; - int m_dpi = 96; - qreal m_devicePixelRatio = 1.0; - QSize m_logicalSize; - QSize m_physicalSize; - bool m_hasInjectedParams = false; -}; - -/** - * @brief 便捷函数 - */ -inline int dp(int value) { return DPIManager::instance()->dp(value); } -inline int sp(int value) { return DPIManager::instance()->sp(value); } - -} // namespace CFDesktop::Base -```text - -### 5.2 DPI 计算公式 - -```cpp -int DPIManager::dp(int value) const { - // 基准 DPI 为 160 - const qreal BASE_DPI = 160.0; - return qRound(value * m_dpi / BASE_DPI * m_devicePixelRatio); -} - -int DPIManager::sp(int value) const { - // sp 考虑字体缩放因子 - const qreal BASE_DPI = 160.0; - qreal fontScale = QApplication::font().pointSizeF() / 10.0; - return qRound(value * m_dpi / BASE_DPI * m_devicePixelRatio * fontScale); -} -```text - -### 5.3 常用尺寸定义 - -```cpp -namespace CFDesktop::Base::Sizes { - // 间距 (使用 dp) - inline int tiny() { return dp(2); } - inline int small() { return dp(4); } - inline int normal() { return dp(8); } - inline int medium() { return dp(16); } - inline int large() { return dp(24); } - inline int xlarge() { return dp(32); } - inline int xxlarge() { return dp(48); } - - // 圆角 - inline int radiusSmall() { return dp(2); } - inline int radiusMedium() { return dp(4); } - inline int radiusLarge() { return dp(8); } - inline int radiusXLarge() { return dp(16); } - - // 控件尺寸 - inline int buttonHeight() { return dp(40); } - inline int inputHeight() { return dp(36); } - inline int iconSizeSmall() { return dp(16); } - inline int iconSizeMedium() { return dp(24); } - inline int iconSizeLarge() { return dp(48); } -} -```yaml - ---- +**DPI 策略**采用 Android 风格的 dp/sp 单位系统,以 160dpi 为基准密度。选择这套方案而非 Qt 原生的 `QScreen::logicalDotsPerInch()` 直接缩放,是因为嵌入式设备的屏幕参数差异极大(从 7 寸 800x480 到 15 寸 1920x1080),dp/sp 提供了更可控的物理尺寸一致性。DPIManager 支持模拟器注入接口,便于在没有真实屏幕的开发机上调试布局。 -## 六、配置中心 (ConfigStore) +**ConfigStore** 采用四层优先级存储(Temp > App > User > System)。Temp 层纯内存,用于运行时临时状态;App 层随应用安装,提供默认配置;User 层存储用户偏好;System 层为系统管理员预留全局策略。四层覆盖而非单一配置文件,是因为嵌入式场景下需要区分「出厂默认」和「用户修改」,且系统管理员可能需要锁死某些策略(如禁用动画以节省电量)。 -### 6.1 ConfigStore 类接口 +**Logger** 采用异步 MPSC(多生产者单消费者)队列设计。日志写入是高频操作,如果在主线程同步写文件会严重影响 UI 流畅度。多线程通过 lock-free 队列提交日志消息,独立的后台消费者线程负责格式化和写入多个 Sink(文件、控制台、网络 UDP)。这确保了日志的性能影响始终低于 1%。 -**文件**: `include/CFDesktop/Base/ConfigStore/ConfigStore.h` +## 关键决策 -```cpp -#pragma once -#include -#include -#include -#include -#include "ConfigNode.h" - -namespace CFDesktop::Base { - -/** - * @brief 配置类型 - */ -enum class ConfigType { - System, // ⚠️ 过时:实际使用 CFDesktop 自管理目录,非 /etc/ - User, // 用户配置 (~/.config/CFDesktop/config.conf) - App, // 应用配置 - Temp // 临时配置 (内存) -}; - -/** - * @brief 配置中心 - * - * 提供层级化的配置存储,支持命名空间和变更监听。 - */ -class ConfigStore : public QObject { - Q_OBJECT - -public: - static ConfigStore* instance(); - - /** - * @brief 获取配置值 - * @param key 配置键,支持点分隔命名空间 (如 "ui.theme.name") - * @param defaultValue 默认值 - */ - QVariant get( - const QString& key, - const QVariant& defaultValue = QVariant() - ) const; - - /** - * @brief 设置配置值 - * @param key 配置键 - * @param value 配置值 - * @param type 配置类型 - */ - void set( - const QString& key, - const QVariant& value, - ConfigType type = ConfigType::User - ); - - /** - * @brief 删除配置 - */ - void remove(const QString& key, ConfigType type = ConfigType::User); - - /** - * @brief 检查配置是否存在 - */ - bool contains(const QString& key) const; - - /** - * @brief 获取配置节点 - */ - ConfigNode node(const QString& path) const; - - /** - * @brief 保存配置 - */ - void save(ConfigType type = ConfigType::User); - - /** - * @brief 重新加载配置 - */ - void reload(ConfigType type = ConfigType::User); - - /** - * @brief 设置配置变更监听 - * @param key 监听的键 - * @param receiver 接收对象 - * @param slot 槽函数 - */ - void watch( - const QString& key, - QObject* receiver, - const char* slot - ); - -signals: - /** - * @brief 配置变化信号 - */ - void configChanged(const QString& key, const QVariant& value); - -private: - ConfigStore(QObject* parent = nullptr); - ~ConfigStore(); - - void initialize(); - QString normalizeKey(const QString& key) const; - QSettings* settingsForType(ConfigType type) const; - -private: - static ConfigStore* s_instance; - QScopedPointer m_systemSettings; - QScopedPointer m_userSettings; - QHash m_tempSettings; - QMultiHash> m_watchers; -}; - -/** - * @brief 便捷访问函数 - */ -template -T getConfig(const QString& key, const T& defaultValue = T()) { - return ConfigStore::instance()->get(key, QVariant::fromValue(defaultValue)).value(); -} - -template -void setConfig(const QString& key, const T& value) { - ConfigStore::instance()->set(key, QVariant::fromValue(value)); -} - -} // namespace CFDesktop::Base -```text - -### 6.2 配置文件格式 - -**系统配置**: ~~`/etc/CFDesktop/config.conf`~~ ⚠️ 过时:实际使用 CFDesktop 自管理目录 - -```ini -[General] -Version=1.0 - -[Hardware] -Tier=auto -MaxMemoryMB=512 - -[UI] -Theme=default -EnableAnimations=auto -RenderingBackend=auto - -[Network] -AutoConnect=true -UpdateCheck=true - -[Logging] -Level=Info -MaxFileSizeMB=10 -MaxFiles=5 -```text - -**用户配置**: `~/.config/CFDesktop/config.conf` - -```ini -[General] -Language=zh_CN - -[UI] -Theme=dark -EnableAnimations=true -StatusBarPosition=top - -[User] -Name=User -AutoLogin=false -```yaml - ---- - -## 七、日志系统 (Logger) - -### 7.1 Logger 类接口 - -**文件**: `include/CFDesktop/Base/Logger/Logger.h` - -```cpp -#pragma once -#include -#include -#include "LogMessage.h" - -namespace CFDesktop::Base { - -/** - * @brief 日志等级 - */ -enum class LogLevel { - Trace = 0, - Debug, - Info, - Warning, - Error, - Fatal -}; - -/** - * @brief 日志输出抽象基类 - */ -class LogSink { -public: - virtual ~LogSink() = default; - virtual void write(const LogMessage& message) = 0; - virtual void flush() = 0; -}; - -/** - * @brief 日志主类 - * - * 提供统一的日志接口,支持多个输出目标。 - */ -class Logger : public QObject { - Q_OBJECT - -public: - static Logger* instance(); - - /** - * @brief 记录日志 - */ - void log(LogLevel level, const QString& tag, const QString& message); - - /** - * @brief 添加日志输出 - */ - void addSink(LogSink* sink); - - /** - * @brief 移除日志输出 - */ - void removeSink(LogSink* sink); - - /** - * @brief 设置最低日志等级 - */ - void setMinLevel(LogLevel level); - - /** - * @brief 获取最低日志等级 - */ - LogLevel minLevel() const; - - /** - * @brief 设置日志标签过滤器 - * - * 只有标签在列表中的日志才会被记录。 - * 空列表表示不过滤。 - */ - void setTagFilter(const QStringList& tags); - - /** - * @brief 刷新所有日志输出 - */ - void flush(); - - // 便捷方法 - void trace(const QString& tag, const QString& message); - void debug(const QString& tag, const QString& message); - void info(const QString& tag, const QString& message); - void warning(const QString& tag, const QString& message); - void error(const QString& tag, const QString& message); - void fatal(const QString& tag, const QString& message); - -signals: - /** - * @brief 日志写入信号 - */ - void messageLogged(const LogMessage& message); - -private: - Logger(QObject* parent = nullptr); - ~Logger() = default; - - QString formatMessage(const LogMessage& message) const; - QString levelToString(LogLevel level) const; - -private: - static Logger* s_instance; - QList m_sinks; - LogLevel m_minLevel = LogLevel::Info; - QStringList m_tagFilters; - QMutex m_mutex; -}; - -// 便捷宏 -#define LOG_TRACE(tag, msg) Logger::instance()->trace(tag, msg) -#define LOG_DEBUG(tag, msg) Logger::instance()->debug(tag, msg) -#define LOG_INFO(tag, msg) Logger::instance()->info(tag, msg) -#define LOG_WARNING(tag, msg) Logger::instance()->warning(tag, msg) -#define LOG_ERROR(tag, msg) Logger::instance()->error(tag, msg) -#define LOG_FATAL(tag, msg) Logger::instance()->fatal(tag, msg) - -} // namespace CFDesktop::Base -```text - -### 7.2 LogMessage 结构 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 日志消息结构 - */ -struct LogMessage { - LogLevel level; // 日志等级 - QString tag; // 标签 - QString message; // 消息内容 - QDateTime timestamp; // 时间戳 - QString threadId; // 线程 ID - QString sourceFile; // 源文件 - int sourceLine; // 源文件行号 - QString function; // 函数名 - - QString toString() const; -}; - -} // namespace CFDesktop::Base -```text - -### 7.3 LogSink 实现 - -#### FileSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 文件日志输出 - */ -class FileSink : public LogSink { -public: - explicit FileSink(const QString& filePath); - ~FileSink() override; - - void write(const LogMessage& message) override; - void flush() override; - - void setMaxFileSize(int bytes); - void setMaxFiles(int count); - -private: - void checkRotation(); - void rotate(); - -private: - QFile m_file; - QTextStream m_stream; - int m_maxFileSize = 10 * 1024 * 1024; // 10MB - int m_maxFiles = 5; - QMutex m_mutex; -}; - -} // namespace CFDesktop::Base -```text - -#### ConsoleSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 控制台日志输出 - * - * 支持彩色输出。 - */ -class ConsoleSink : public LogSink { -public: - explicit ConsoleSink(bool enableColor = true); - ~ConsoleSink() override = default; - - void write(const LogMessage& message) override; - void flush() override; - -private: - QString colorize(const QString& text, LogLevel level) const; - -private: - bool m_enableColor; -}; - -} // namespace CFDesktop::Base -```text - -#### NetworkSink - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 网络日志输出 - * - * 通过 UDP 发送日志到远程服务器。 - */ -class NetworkSink : public LogSink { -public: - explicit NetworkSink(const QString& host, quint16 port); - ~NetworkSink() override; - - void write(const LogMessage& message) override; - void flush() override; - -private: - QUdpSocket m_socket; - QString m_host; - quint16 m_port; -}; - -} // namespace CFDesktop::Base -```text - -### 7.4 日志配置 - -**文件**: `configs/logging.conf` - -```ini -[General] -Level=Debug -# 过滤标签,空表示不过滤 -TagFilters=HardwareProbe,ThemeEngine,AnimationManager - -[Console] -Enabled=true -Color=true - -[File] -Enabled=true -Path=/var/log/CFDesktop/desktop.log -MaxFileSizeMB=10 -MaxFiles=5 - -[Network] -Enabled=false -Host=192.168.1.100 -Port=514 -```yaml - ---- - -## 八、详细任务清单 - -### 8.1 Week 1: 主题引擎 - -#### Day 1-2: 基础结构 -- [ ] 创建 ThemeEngine 类框架 -- [ ] 定义主题数据结构 -- [ ] 实现主题加载器 -- [ ] 创建默认主题包 - -#### Day 3: 变量系统 -- [ ] 实现变量解析器 -- [ ] 支持点分隔路径 -- [ ] 实现变量继承 - -#### Day 4: QSS 处理 -- [ ] 实现 QSS 变量替换 -- [ ] 实现 QSS 合并 -- [ ] 支持主题继承 - -#### Day 5: 集成与测试 -- [ ] 集成到应用 -- [ ] 编写单元测试 -- [ ] 性能优化 - -### 8.2 Week 2: 动画管理器 - -#### Day 1-2: 核心功能 -- [ ] 创建 AnimationManager 类 -- [ ] 实现基础动画创建 -- [ ] 实现 HWTier 降级逻辑 - -#### Day 3: 预定义动画 -- [ ] 实现淡入淡出 -- [ ] 实现滑动动画 -- [ ] 实现缩放动画 -- [ ] 实现旋转动画 - -#### Day 4: 组动画 -- [ ] 实现并行动画组 -- [ ] 实现串行动画组 -- [ ] 实现动画生命周期管理 - -#### Day 5: 测试与优化 -- [ ] 编写单元测试 -- [ ] 性能测试 -- [ ] 内存泄漏检查 - -### 8.3 Week 3: DPI 管理器与配置中心 - -#### Day 1-2: DPI 管理器 -- [ ] 创建 DPIManager 类 -- [ ] 实现屏幕检测 -- [ ] 实现 dp/sp 转换 -- [ ] 实现模拟器注入接口 - -#### Day 3-4: 配置中心 -- [ ] 创建 ConfigStore 类 -- [ ] 实现层级存储 -- [ ] 实现变更监听 -- [ ] 实现配置持久化 - -#### Day 5: 测试 -- [ ] 跨平台测试 -- [ ] 单元测试 -- [ ] 集成测试 - -### 8.4 Week 4: 日志系统与集成 - -#### Day 1-2: 日志系统 -- [ ] 创建 Logger 类 -- [ ] 实现 FileSink -- [ ] 实现 ConsoleSink -- [ ] 实现 NetworkSink - -#### Day 3: 日志增强 -- [ ] 实现日志轮转 -- [ ] 实现标签过滤 -- [ ] 实现彩色输出 - -#### Day 4: 集成测试 -- [ ] 全模块集成 -- [ ] 端到端测试 -- [ ] 性能测试 - -#### Day 5: 文档与发布 -- [ ] API 文档 -- [ ] 使用示例 -- [ ] Code Review - ---- - -## 九、验收标准 - -### 9.1 主题引擎 -- [ ] 支持动态切换主题 -- [ ] 支持变量继承 -- [ ] Low 档位自动禁用阴影/圆角/渐变 -- [ ] 热重载时间 < 100ms - -### 9.2 动画管理器 -- [ ] Low 档位动画时长归零 -- [ ] 所有预定义动画正常工作 -- [ ] 无内存泄漏 -- [ ] 动画不阻塞主线程 - -### 9.3 DPI 管理器 -- [ ] dp/sp 转换准确 -- [ ] 支持模拟器注入 -- [ ] 热插拔屏幕自动适配 - -### 9.4 配置中心 -- [ ] 层级存储正常 -- [ ] 变更监听触发 -- [ ] 持久化正常 - -### 9.5 日志系统 -- [ ] 多 Sink 并发安全 -- [ ] 日志轮转正常 -- [ ] 性能影响 < 1% - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 主题引擎五层流水线 | 各层独立测试、Low 档可跳过高层的 Material 效果 | 单体 ThemeEngine 类(职责过重、难以裁剪) | +| dp/sp 密度无关单位 (160dpi 基准) | 物理尺寸一致性好、嵌入式屏幕参数差异大时仍可控 | 直接使用 Qt DPI 缩放(不同设备表现不一致) | +| ConfigStore 四层覆盖 (Temp/App/User/System) | 区分出厂默认与用户修改、支持管理员锁定策略 | 单一 JSON 配置文件(无法表达优先级和锁定) | +| Logger 异步 MPSC 队列 | 日志写入不阻塞主线程、多线程安全、性能影响 <1% | 同步写文件(主线程卡顿)、spdlog(引入外部依赖) | +| 主题降级由 HWTier 驱动 | Low 档自动禁用阴影/模糊/动画,Mid 档简化效果 | 手动配置每个视觉效果(用户负担重、易遗漏) | -## 十、下一步行动 +## 当前状态 -完成 Phase 2 后,进入 **Phase 3: 输入抽象层**。 +部分实现 -- ThemeEngine、AnimationManager、ConfigStore、Logger 已完成;DPIManager 尚未实现。UI 层已从 Phase 2 中分离为独立的 `ui/` 模块并完成 95%(P0/P1 widgets)。详见 `document/design_stage/status/`。 diff --git a/document/design_stage/03_phase3_input_layer.md b/document/design_stage/03_phase3_input_layer.md index 2c52ea5b0..302715eb5 100644 --- a/document/design_stage/03_phase3_input_layer.md +++ b/document/design_stage/03_phase3_input_layer.md @@ -1,1452 +1,29 @@ --- title: "Phase 3: 输入抽象层详细设计文档" -description: "完成 Phase 3 后,进入 Phase 4: Shell UI 主体。" +description: "统一触摸、按键、旋钮等输入事件,支持焦点导航和手势识别" --- -# Phase 3: 输入抽象层详细设计文档 +# Phase 3: 输入抽象层 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 3 - 输入抽象层 | -| 预计周期 | 1~2 周 | -| 依赖阶段 | Phase 0, Phase 1, Phase 2 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -屏蔽底层输入差异,统一触摸、物理按键、旋钮等输入事件,支持焦点导航模式。 - -### 1.2 具体交付物 -- [ ] `InputManager` 统一分发层 -- [ ] `TouchInputHandler` 触摸处理器 -- [ ] `KeyInputHandler` 按键处理器 -- [ ] `RotaryInputHandler` 旋钮处理器 -- [ ] `FocusNavigator` 焦点导航器 -- [ ] 单元测试 - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (Widgets receive events) │ -├─────────────────────────────────────────────────────────────┤ -│ Input Manager │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 事件转换与分发 │ │ -│ │ - 统一事件接口 │ │ -│ │ - 事件过滤与拦截 │ │ -│ │ - 手势识别 │ │ -│ └────────────────────────────────────────────────────┘ │ -├──────────────────────┬──────────────────────────────────────┤ -│ Native Input Sources│ Qt Input Sources │ -│ ┌────────────────┐ │ ┌─────────────────────────────────┐ │ -│ │ evdev Devices │ │ │ QTouchEvent (linuxfb/eglfs) │ │ -│ │ /dev/input/event│ │ │ QKeyEvent (键盘) │ │ -│ │ GPIO Buttons │ │ │ QMouseEvent (鼠标) │ │ -│ │ Rotary Encoder │ │ │ QWheelEvent (滚轮) │ │ -│ └────────────────┘ │ └─────────────────────────────────┘ │ -├──────────────────────┴──────────────────────────────────────┤ -│ Kernel / Driver Layer │ -│ evdev, GPIO, I2C, SPI input drivers │ -└─────────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/base/ -├── include/CFDesktop/Base/Input/ -│ ├── InputManager.h # 输入管理器主类 -│ ├── InputEvent.h # 统一事件结构 -│ ├── TouchInputHandler.h # 触摸处理器 -│ ├── KeyInputHandler.h # 按键处理器 -│ ├── RotaryInputHandler.h # 旋钮处理器 -│ ├── GestureRecognizer.h # 手势识别器 -│ ├── FocusNavigator.h # 焦点导航器 -│ └── InputDevice.h # 输入设备抽象 -│ -└── src/input/ - ├── InputManager.cpp - ├── TouchInputHandler.cpp - ├── KeyInputHandler.cpp - ├── RotaryInputHandler.cpp - ├── GestureRecognizer.cpp - ├── FocusNavigator.cpp - ├── native/ # 平台特定实现 - │ ├── EvdevDevice.cpp # Linux evdev 设备 - │ ├── GPIOButton.cpp # GPIO 按键 - │ └── RotaryEncoder.cpp # 旋转编码器 - └── simulator/ # 模拟器支持 - └── SimulatedInput.cpp -```yaml - ---- - -## 三、统一事件结构 - -### 3.1 输入事件类型 - -**文件**: `include/CFDesktop/Base/Input/InputEvent.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Base { - -/** - * @brief 输入设备类型 - */ -enum class InputDeviceType { - Unknown, - Touchscreen, # 触摸屏 - Mouse, # 鼠标 - Keyboard, # 键盘 - PhysicalButton, # 物理按键 - RotaryEncoder, # 旋转编码器 - Gamepad, # 游戏手柄 - Custom # 自定义设备 -}; - -/** - * @brief 统一输入事件基类 - */ -class InputEvent { -public: - InputDeviceType deviceType = InputDeviceType::Unknown; - QString deviceId; # 设备 ID - quint64 timestamp; # 时间戳 (微秒) - - virtual ~InputEvent() = default; - virtual QEvent* toQtEvent() const = 0; -}; - -/** - * @brief 触摸/指针位置事件 - */ -class PointerEvent : public InputEvent { -public: - enum class Type { - Press, - Release, - Move, - Enter, - Leave - }; - - Type type; - QPointF position; # 屏幕坐标 - int pointerId = -1; # 多点触摸 ID - qreal pressure = 0.0; # 压力 (0.0 - 1.0) - int buttons = 0; # 按钮状态 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 按键事件 - */ -class KeyEvent : public InputEvent { -public: - enum class Type { - Press, - Release, - Repeat, - LongPress, # 长按 - MultiPress # 多连击 - }; - - Type type; - int keyCode; # Qt Key_Code - quint32 nativeCode; # 原生扫描码 - QString text; # 文本内容 - bool isAutoRepeat = false; - int clickCount = 0; # 连击计数 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 旋钮事件 - */ -class RotaryEvent : public InputEvent { -public: - enum class Direction { - Clockwise, - CounterClockwise - }; - - Direction direction; - int steps = 1; # 步数 - qreal angleDelta = 0.0; # 角度变化 - bool isPressed = false; # 是否被按下 - int clickCount = 0; # 旋转档位 - - QEvent* toQtEvent() const override; -}; - -/** - * @brief 手势事件 - */ -class GestureEvent : public InputEvent { -public: - enum class Type { - Swipe, - Pinch, - Rotate, - LongPress, - DoubleTap, - Custom - }; - - Type type; - QVariantMap data; # 手势特定数据 - QPointF centerPoint; - qreal angle = 0.0; - qreal scale = 1.0; - QPointF delta; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 四、输入管理器 (InputManager) - -### 4.1 InputManager 类接口 - -**文件**: `include/CFDesktop/Base/Input/InputManager.h` - -```cpp -#pragma once -#include -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -class InputDevice; -class TouchInputHandler; -class KeyInputHandler; -class RotaryInputHandler; -class GestureRecognizer; - -/** - * @brief 输入管理器 - * - * 统一管理所有输入设备,提供事件分发和过滤。 - * 单例模式,在应用启动时初始化。 - */ -class InputManager : public QObject { - Q_OBJECT - -public: - static InputManager* instance(); - - /** - * @brief 初始化输入管理器 - */ - bool initialize(); - - /** - * @brief 注册输入设备 - */ - void registerDevice(InputDevice* device); - - /** - * @brief 注销输入设备 - */ - void unregisterDevice(InputDevice* device); - - /** - * @brief 获取所有设备 - */ - QList devices() const; - - /** - * @brief 获取指定类型的设备 - */ - QList devices(InputDeviceType type) const; - - /** - * @brief 添加事件过滤器 - * - * 过滤器返回 true 表示事件被拦截。 - */ - void addEventFilter(QObject* filter); - - /** - * @brief 移除事件过滤器 - */ - void removeEventFilter(QObject* filter); - - /** - * @brief 分发事件到指定对象 - */ - bool dispatchEvent(QObject* receiver, InputEvent* event); - - /** - * @brief 启用/禁用输入 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - -signals: - /** - * @brief 设备连接信号 - */ - void deviceConnected(InputDevice* device); - - /** - * @brief 设备断开信号 - */ - void deviceDisconnected(InputDevice* device); - - /** - * @brief 输入事件信号(用于调试) - */ - void inputEventReceived(InputEvent* event); - -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private: - InputManager(QObject* parent = nullptr); - ~InputManager() = default; - - void setupNativeInputDevices(); - void processQtEvent(QEvent* event); - -private: - static InputManager* s_instance; - QList> m_devices; - QList> m_eventFilters; - QPointer m_touchHandler; - QPointer m_keyHandler; - QPointer m_rotaryHandler; - QPointer m_gestureRecognizer; - bool m_enabled = true; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 五、触摸处理器 (TouchInputHandler) - -### 5.1 TouchInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/TouchInputHandler.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 触摸点状态 - */ -struct TouchPoint { - int id; - QPointF position; - QPointF startPosition; - qreal pressure = 0.0; - qint64 startTime = 0; - bool isPressed = false; -}; - -/** - * @brief 触摸处理器 - * - * 处理 Qt 原生触摸事件,转换为统一事件格式。 - * 支持多点触摸和手势识别。 - */ -class TouchInputHandler : public QObject { - Q_OBJECT - -public: - explicit TouchInputHandler(QObject* parent = nullptr); - ~TouchInputHandler() = default; - - /** - * @brief 处理 Qt 触摸事件 - */ - bool handleEvent(QTouchEvent* event); - - /** - * @brief 启用/禁用触摸输入 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - - /** - * @brief 设置点击判定阈值 - * - * 移动距离小于此值视为点击。 - */ - void setClickThreshold(qreal pixels); - - /** - * @brief 设置长按超时 - */ - void setLongPressTimeout(int milliseconds); - - /** - * @brief 获取当前触摸点 - */ - QHash activeTouches() const; - -signals: - /** - * @brief 触摸事件信号 - */ - void touchEvent(PointerEvent* event); - - /** - * @brief 单击信号 - */ - void singleTap(const QPointF& position); - - /** - * @brief 双击信号 - */ - void doubleTap(const QPointF& position); - - /** - * @brief 长按信号 - */ - void longPress(const QPointF& position); - -private: - void updateTouchPoints(const QTouchEvent::TouchPoint& point); - void detectGestures(); - void resetGestureState(); - -private: - bool m_enabled = true; - qreal m_clickThreshold = 10.0; # dp(10) - int m_longPressTimeout = 500; - QHash m_activeTouches; - QTimer* m_longPressTimer; - QPointF m_lastPosition; -}; - -} // namespace CFDesktop::Base -```text - -### 5.2 触摸事件流程图 - -```text -QTouchEvent - │ - ├─> TouchPress - │ │ - │ ├─> 记录触摸点 - │ ├─> 启动长按定时器 - │ └─> emit touchEvent() - │ - ├─> TouchMove - │ │ - │ ├─> 更新触摸点位置 - │ ├─> 检测是否超过点击阈值 - │ ├─> 如果超过阈值,取消长按定时器 - │ └─> emit touchEvent() - │ - └─> TouchRelease - │ - ├─> 计算移动距离 - ├─> 如果距离小且时间短:单击 - ├─> 如果是第二次单击:双击 - ├─> 如果定时器触发:长按 - └─> 清理触摸点 -```yaml - ---- - -## 六、按键处理器 (KeyInputHandler) - -### 6.1 KeyInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/KeyInputHandler.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 按键配置 - */ -struct KeyConfig { - int keyCode; # Qt 键码 - QString action; # 动作名称 - int longPressThreshold = 500; # 长按阈值 (毫秒) - int repeatDelay = 300; # 重复延迟 - int repeatInterval = 100; # 重复间隔 - bool supportLongPress = true; - bool supportMultiPress = true; - int maxMultiPressCount = 3; # 最大连击数 -}; - -/** - * @brief 按键处理器 - * - * 处理键盘和物理按键输入,支持长按、连击检测。 - */ -class KeyInputHandler : public QObject { - Q_OBJECT - -public: - explicit KeyInputHandler(QObject* parent = nullptr); - ~KeyInputHandler() = default; - - /** - * @brief 处理 Qt 按键事件 - */ - bool handleEvent(QKeyEvent* event); - - /** - * @brief 注册按键配置 - */ - void registerKey(const KeyConfig& config); - - /** - * @brief 取消按键注册 - */ - void unregisterKey(int keyCode); - - /** - * @brief 映射物理按键到 Qt 键码 - * - * 用于 evdev 扫描码到 Qt Key 的映射。 - */ - void mapKey(int nativeScanCode, int qtKeyCode); - - /** - * @brief 获取当前按下的按键 - */ - QList pressedKeys() const; - -signals: - /** - * @brief 按键事件信号 - */ - void keyEvent(KeyEvent* event); - - /** - * @brief 动作触发信号 - */ - void actionTriggered(const QString& action, int count); - - /** - * @brief 长按信号 - */ - void longPress(int keyCode); - - /** - * @brief 组合键信号 - */ - void combinationPressed(const QList& keyCodes); - -private: - void handleKeyPress(QKeyEvent* event); - void handleKeyRelease(QKeyEvent* event); - bool detectCombination(); - -private: - QHash m_keyConfigs; - QHash m_scanCodeToQtKey; - QList m_pressedKeys; - QHash m_keyPressTime; - QHash m_keyPressCount; - QHash m_longPressTimers; - QHash m_repeatTimers; -}; - -} // namespace CFDesktop::Base -```text - -### 6.2 预定义按键映射 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 预定义按键动作 - */ -enum class StandardAction { - // 导航 - Up, - Down, - Left, - Right, - Enter, - Back, - Home, - Menu, - - // 媒体 - VolumeUp, - VolumeDown, - Mute, - PlayPause, - Next, - Previous, - - // 系统 - Power, - Sleep, - WakeUp, - - // 自定义 - Custom1, - Custom2, - Custom3, - Custom4 -}; - -/** - * @brief 标准按键映射表 - */ -struct StandardKeyMapping { - static void initializeDefaults(KeyInputHandler* handler); - - // GPIO 按键默认映射 - static const QHash gpioButtonMap; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 七、旋钮处理器 (RotaryInputHandler) - -### 7.1 RotaryInputHandler 类接口 - -**文件**: `include/CFDesktop/Base/Input/RotaryInputHandler.h` - -```cpp -#pragma once -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 旋钮配置 - */ -struct RotaryConfig { - int deviceId = 0; - int stepsPerRevolution = 24; # 每圈步数 - bool hasButton = true; # 是否带按钮 - qreal acceleration = 1.0; # 加速因子 - int velocitySamples = 3; # 速度采样数 -}; - -/** - * @brief 旋钮处理器 - * - * 处理旋转编码器输入,支持加速和按钮模式。 - */ -class RotaryInputHandler : public QObject { - Q_OBJECT - -public: - explicit RotaryInputHandler(QObject* parent = nullptr); - ~RotaryInputHandler() = default; - - /** - * @brief 处理旋钮事件 - */ - bool handleEvent(int delta, bool buttonPressed); - - /** - * @brief 设置旋钮配置 - */ - void setConfig(const RotaryConfig& config); - - /** - * @brief 获取当前旋转速度 - * - * 返回步数/秒 - */ - qreal currentVelocity() const; - - /** - * @brief 获取累计旋转角度 - */ - qreal accumulatedAngle() const; - - /** - * @brief 重置累计角度 - */ - void resetAccumulatedAngle(); - -signals: - /** - * @brief 旋转事件信号 - */ - void rotated(RotaryEvent* event); - - /** - * @brief 简单旋转信号 - */ - void stepped(int steps, RotaryEvent::Direction direction); - - /** - * @brief 按钮按下信号 - */ - void buttonPressed(); - - /** - * @brief 按钮释放信号 - */ - void buttonReleased(); - - /** - * @brief 速度变化信号 - */ - void velocityChanged(qreal velocity); - -private: - void updateVelocity(); - int calculateAcceleratedSteps(int rawSteps); - -private: - RotaryConfig m_config; - qreal m_accumulatedAngle = 0.0; - QList m_timestampSamples; - QList m_deltaSamples; - qreal m_currentVelocity = 0.0; - bool m_buttonPressed = false; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 八、手势识别器 (GestureRecognizer) - -### 8.1 GestureRecognizer 类接口 - -**文件**: `include/CFDesktop/Base/Input/GestureRecognizer.h` +CFDesktop 面向嵌入式工控屏、平板和桌面多种形态,输入源差异极大:低端工控屏仅有电阻触摸+GPIO 按键,中端设备带旋转编码器,桌面端则是鼠标键盘。如果不做输入抽象,每个 Widget 必须分别处理 QTouchEvent、QKeyEvent、QWheelEvent 以及原生 evdev/GPIO 事件,导致大量重复代码且难以保证跨设备行为一致。 -```cpp -#pragma once -#include -#include -#include "InputEvent.h" +因此设计了 `InputManager` 单一分发层:所有输入源(Qt 事件、evdev 设备、GPIO、旋转编码器)先被各自的 Handler 转换为统一的 `InputEvent` 体系(PointerEvent / KeyEvent / RotaryEvent / GestureEvent),再由 InputManager 统一分发到目标控件。这样 Widget 层只对接一套事件接口,新增输入类型只需添加新的 Handler,不影响上层。 -namespace CFDesktop::Base { +手势识别(GestureRecognizer)从触摸点流中检测 Swipe/Pinch/LongPress/DoubleTap 等手势,将结果作为 GestureEvent 分发。焦点导航(FocusNavigator)为无触摸设备提供基于空间距离的方向导航算法,支持自定义焦点链和循环策略。模拟器通过 `SimulatedInputConfig` 将鼠标/键盘映射为触摸/旋钮事件,确保开发机和目标设备行为一致。 -/** - * @brief 手势类型 - */ -enum class GestureType { - None, - Swipe, - Pinch, - Rotate, - LongPress, - DoubleTap, - TwoFingerTap, - ThreeFingerSwipe, - Custom -}; +## 关键决策 -/** - * @brief 手势方向 - */ -enum class GestureDirection { - Up, - Down, - Left, - Right, - In, - Out, - Clockwise, - CounterClockwise -}; - -/** - * @brief 手势识别结果 - */ -struct GestureResult { - GestureType type = GestureType::None; - GestureDirection direction = GestureDirection::Up; - QPointF centerPoint; - QPointF displacement; # 位移 - qreal scale = 1.0; # 缩放比例 - qreal angle = 0.0; # 旋转角度 - qint64 duration = 0; # 持续时间 - int fingerCount = 1; # 触摸点数 -}; - -/** - * @brief 手势配置 - */ -struct GestureConfig { - int swipeMinDistance = 50; # 最小滑动距离 (dp) - int swipeMaxDeviation = 30; # 最大偏离 (dp) - int swipeTimeout = 500; # 滑动超时 (毫秒) - - int pinchMinDistance = 20; # 最小捏合距离 - qreal pinchMinScale = 0.5; # 最小缩放比例 - - int longPressTimeout = 500; # 长按超时 - int longPressMaxMovement = 10; # 长按最大移动 - - int doubleTapInterval = 300; # 双击间隔 - int doubleTapMaxMovement = 20; # 双击最大移动 - - bool enableAll = true; # 启用所有手势 -}; - -/** - * @brief 手势识别器 - * - * 从触摸点流中识别常见手势。 - */ -class GestureRecognizer : public QObject { - Q_OBJECT - -public: - explicit GestureRecognizer(QObject* parent = nullptr); - ~GestureRecognizer() = default; - - /** - * @brief 处理触摸点更新 - */ - void processTouchPoints(const QHash& points); - - /** - * @brief 设置手势配置 - */ - void setConfig(const GestureConfig& config); - - /** - * @brief 启用/禁用特定手势 - */ - void setGestureEnabled(GestureType type, bool enabled); - - /** - * @brief 取消当前手势 - */ - void cancelCurrentGesture(); - -signals: - /** - * @brief 手势识别信号 - */ - void gestureRecognized(const GestureResult& gesture); - - /** - * @brief 手势开始信号 - */ - void gestureStarted(GestureType type); - - /** - * @brief 手势更新信号 - */ - void gestureUpdated(const GestureResult& gesture); - - /** - * @brief 手势结束信号 - */ - void gestureEnded(const GestureResult& gesture); - - /** - * @brief 手势取消信号 - */ - void gestureCancelled(GestureType type); - -private: - bool detectSwipe(); - bool detectPinch(); - bool detectRotate(); - bool detectLongPress(); - bool detectDoubleTap(); - GestureDirection detectDirection(const QPointF& delta); - - void reset(); - -private: - GestureConfig m_config; - QSet m_enabledGestures; - QHash m_activeTouches; - QList m_startPoints; - GestureResult m_currentGesture; - qint64 m_gestureStartTime = 0; - bool m_isGestureActive = false; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 九、焦点导航器 (FocusNavigator) - -### 9.1 FocusNavigator 类接口 - -**文件**: `include/CFDesktop/Base/Input/FocusNavigator.h` - -```cpp -#pragma once -#include -#include -#include "InputEvent.h" - -namespace CFDesktop::Base { - -/** - * @brief 焦点移动方向 - */ -enum class FocusDirection { - Up, - Down, - Left, - Right, - Next, # Tab 顺序下一个 - Previous, # Tab 顺序上一个 - First, # 第一个控件 - Last # 最后一个控件 -}; - -/** - * @brief 焦点策略 - */ -enum class FocusPolicy { - // 自动策略 - Auto, - - // 严格策略:只跳到可视控件 - Strict, - - // 包含策略:包含隐藏/禁用控件 - Inclusive, - - // 循环策略:边界循环 - Wrap, - - // 自定义策略 - Custom -}; - -/** - * @brief 焦点导航器 - * - * 为无触摸设备提供键盘/按键焦点导航。 - */ -class FocusNavigator : public QObject { - Q_OBJECT - -public: - static FocusNavigator* instance(); - - /** - * @brief 设置导航根窗口 - */ - void setRootWidget(QWidget* widget); - - /** - * @brief 移动焦点 - */ - bool moveFocus(FocusDirection direction); - - /** - * @brief 设置焦点策略 - */ - void setFocusPolicy(FocusPolicy policy); - - /** - * @brief 添加焦点链 - * - * 自定义焦点跳转顺序。 - */ - void addFocusChain(QWidget* from, QWidget* to, FocusDirection direction); - - /** - * @brief 移除焦点链 - */ - void removeFocusChain(QWidget* from, FocusDirection direction); - - /** - * @brief 设置当前焦点控件 - */ - void setFocusWidget(QWidget* widget); - - /** - * @brief 获取当前焦点控件 - */ - QWidget* currentFocusWidget() const; - - /** - * @brief 获取下一个焦点控件 - */ - QWidget* nextFocusWidget(FocusDirection direction) const; - -signals: - /** - * @brief 焦点变化信号 - */ - void focusChanged(QWidget* oldWidget, QWidget* newWidget); - - /** - * @brief 无焦点可移动信号 - */ - void focusBlocked(QWidget* currentWidget, FocusDirection direction); - -private: - FocusNavigator(QObject* parent = nullptr); - ~FocusNavigator() = default; - - QWidget* findNearestWidget(QWidget* start, FocusDirection direction) const; - bool isFocusable(QWidget* widget) const; - qreal calculateDistance(QWidget* from, QWidget* to, FocusDirection direction) const; - bool isInDirection(QWidget* from, QWidget* to, FocusDirection direction) const; - -private: - static FocusNavigator* s_instance; - QPointer m_rootWidget; - QPointer m_currentWidget; - FocusPolicy m_policy = FocusPolicy::Auto; - QMultiHash> m_focusChains; -}; - -} // namespace CFDesktop::Base -```text - -### 9.2 焦点导航算法 - -```cpp -QWidget* FocusNavigator::findNearestWidget( - QWidget* start, - FocusDirection direction -) const { - if (!start || !m_rootWidget) { - return nullptr; - } - - QWidget* nearest = nullptr; - qreal minDistance = std::numeric_limits::max(); - - // 获取所有可聚焦的子控件 - QList candidates = m_rootWidget->findChildren(); - QRect startRect = start->geometry(); - - for (QWidget* candidate : candidates) { - if (!isFocusable(candidate) || candidate == start) { - continue; - } - - if (!isInDirection(start, candidate, direction)) { - continue; - } - - qreal distance = calculateDistance(start, candidate, direction); - if (distance < minDistance) { - minDistance = distance; - nearest = candidate; - } - } - - return nearest; -} - -bool FocusNavigator::isInDirection( - QWidget* from, - QWidget* to, - FocusDirection direction -) const { - QRect fromRect = from->geometry(); - QRect toRect = to->geometry(); - QPointF fromCenter = fromRect.center(); - QPointF toCenter = toRect.center(); - - switch (direction) { - case FocusDirection::Up: - return toCenter.y() < fromCenter.y(); - case FocusDirection::Down: - return toCenter.y() > fromCenter.y(); - case FocusDirection::Left: - return toCenter.x() < fromCenter.x(); - case FocusDirection::Right: - return toCenter.x() > fromCenter.x(); - default: - return true; - } -} -```yaml - ---- - -## 十、原生输入设备 - -### 10.1 Evdev 设备 - -**文件**: `src/input/native/EvdevDevice.h` - -```cpp -#pragma once -#include -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief Linux evdev 输入设备 - * - * 直接读取 /dev/input/eventX 设备。 - * 支持按键、相对轴(鼠标)、绝对轴(触摸屏)。 - */ -class EvdevDevice : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 设备类型 - */ - enum class EvdevType { - Unknown, - Keyboard, - Mouse, - Touchscreen, - Joystick, - Accelerometer - }; - - /** - * @brief 打开设备 - */ - static EvdevDevice* open(const QString& devicePath); - - ~EvdevDevice() override; - - /** - * @brief 开始读取事件 - */ - bool start() override; - - /** - * @brief 停止读取事件 - */ - void stop() override; - - /** - * @brief 设备类型 - */ - EvdevType evdevType() const; - - /** - * @brief 设备名称 - */ - QString deviceName() const; - -signals: - /** - * @brief evdev 原始事件信号 - */ - void rawEvent(quint16 type, quint16 code, qint32 value); - -private: - EvdevDevice(const QString& path, int fd, QObject* parent = nullptr); - - void readEvents(); - void identifyDevice(); - -private: - QString m_devicePath; - int m_fd = -1; - EvdevType m_type = EvdevType::Unknown; - QString m_name; - QSocketNotifier* m_notifier = nullptr; -}; - -} // namespace CFDesktop::Base -```text - -### 10.2 GPIO 按键 - -**文件**: `src/input/native/GPIOButton.h` - -```cpp -#pragma once -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief GPIO 按键设备 - * - * 通过 sysfs 或 libgpiod 读取 GPIO 按键。 - */ -class GPIOButton : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 打开 GPIO 按键 - * @param gpioPin GPIO 引脚号 - * @param activeLow 低电平有效 - */ - static GPIOButton* open(int gpioPin, bool activeLow = true); - - ~GPIOButton() override; - - bool start() override; - void stop() override; - - /** - * @brief 关联的 Qt 键码 - */ - int mappedKeyCode() const; - void setMappedKeyCode(int keyCode); - - /** - * @brief 防抖延迟 (毫秒) - */ - int debounceDelay() const; - void setDebounceDelay(int milliseconds); - -signals: - /** - * @brief 按钮按下信号 - */ - void pressed(); - - /** - * @brief 按钮释放信号 - */ - void released(); - -private: - GPIOButton(int gpioPin, QObject* parent = nullptr); - - void handleEdgeDetect(); - void setupGPIO(); - -private: - int m_gpioPin; - bool m_activeLow = true; - int m_mappedKeyCode = Qt::Key_unknown; - int m_debounceDelay = 50; - QFile m_valueFile; - QTimer* m_debounceTimer = nullptr; - bool m_lastState = false; -}; - -} // namespace CFDesktop::Base -```text - -### 10.3 旋转编码器 - -**文件**: `src/input/native/RotaryEncoder.h` - -```cpp -#pragma once -#include -#include "InputDevice.h" - -namespace CFDesktop::Base { - -/** - * @brief 旋转编码器设备 - * - * 支持 AB 相位编码器。 - */ -class RotaryEncoder : public InputDevice { - Q_OBJECT - -public: - /** - * @brief 打开旋转编码器 - * @param pinA A 相引脚 - * @param pinB B 相引脚 - */ - static RotaryEncoder* open(int pinA, int pinB); - - ~RotaryEncoder() override; - - bool start() override; - void stop() override; - - /** - * @brief 每圈步数 - */ - void setStepsPerRevolution(int steps); - - /** - * @brief 是否反转方向 - */ - void setInverted(bool inverted); - -signals: - /** - * @brief 旋转信号 - */ - void rotated(int steps); - - /** - * @brief 按钮信号(如果带按钮) - */ - void buttonPressed(); - void buttonReleased(); - -private: - RotaryEncoder(int pinA, int pinB, QObject* parent = nullptr); - - void decodeState(int stateA, int stateB); - -private: - int m_pinA; - int m_pinB; - int m_stepsPerRevolution = 24; - bool m_inverted = false; - int m_lastState = 0; - int m_position = 0; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 十一、模拟器支持 - -### 11.1 模拟输入配置 - -```cpp -namespace CFDesktop::Base { - -/** - * @brief 模拟输入配置 - */ -struct SimulatedInputConfig { - // 鼠标模拟触摸 - bool mouseEmulatesTouch = true; - - // 键盘模拟方向键 - QHash keyToDirection = { - { Qt::Key_Up, FocusDirection::Up }, - { Qt::Key_Down, FocusDirection::Down }, - { Qt::Key_Left, FocusDirection::Left }, - { Qt::Key_Right, FocusDirection::Right }, - { Qt::Key_Tab, FocusDirection::Next }, - { Qt::Key_Backtab, FocusDirection::Previous } - }; - - // 触摸视觉反馈 - bool showTouchRipple = true; - int rippleDuration = 300; - - // 旋钮模拟 - bool mouseWheelEmulatesRotary = true; - qreal wheelScale = 1.0; -}; - -} // namespace CFDesktop::Base -```yaml - ---- - -## 十二、详细任务清单 - -### 12.1 Week 1: 核心处理器 - -#### Day 1-2: InputManager 基础 -- [ ] 创建 InputManager 类 -- [ ] 实现设备注册/注销 -- [ ] 实现事件分发机制 -- [ ] 添加事件过滤器支持 - -#### Day 3: TouchInputHandler -- [ ] 创建 TouchInputHandler 类 -- [ ] 实现触摸点跟踪 -- [ ] 实现单击/双击检测 -- [ ] 实现长按检测 - -#### Day 4: KeyInputHandler -- [ ] 创建 KeyInputHandler 类 -- [ ] 实现按键状态跟踪 -- [ ] 实现长按检测 -- [ ] 实现连击检测 - -#### Day 5: RotaryInputHandler -- [ ] 创建 RotaryInputHandler 类 -- [ ] 实现旋转解码 -- [ ] 实现速度计算 -- [ ] 实现加速功能 - -### 12.2 Week 2: 手势与导航 - -#### Day 1-2: GestureRecognizer -- [ ] 创建 GestureRecognizer 类 -- [ ] 实现滑动手势 -- [ ] 实现捏合手势 -- [ ] 实现旋转手势 - -#### Day 3: FocusNavigator -- [ ] 创建 FocusNavigator 类 -- [ ] 实现方向导航算法 -- [ ] 实现焦点链自定义 -- [ ] 实现边界策略 - -#### Day 4: 原生设备 -- [ ] 实现 EvdevDevice -- [ ] 实现 GPIOButton -- [ ] 实现 RotaryEncoder - -#### Day 5: 测试与集成 -- [ ] 编写单元测试 -- [ ] 集成测试 -- [ ] 性能测试 - ---- - -## 十三、验收标准 - -### 13.1 功能验收 -- [ ] 触摸输入正常响应 -- [ ] 手势识别准确率 > 95% -- [ ] 焦点导航无死循环 -- [ ] 原生设备正常读取 - -### 13.2 性能验收 -- [ ] 事件延迟 < 16ms -- [ ] CPU 占用 < 5% - -### 13.3 兼容性验收 -- [ ] 模拟器和真机行为一致 - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 统一 InputEvent 体系 (PointerEvent/KeyEvent/RotaryEvent/GestureEvent) | Widget 只需对接一套接口;新增输入类型只需加 Handler | 让 Widget 直接处理 Qt 原生事件 + evdev 并行 | +| InputManager 单例 + 事件过滤器链 | 集中管理设备生命周期,支持全局事件拦截(如全局手势) | 每个 Widget 独立注册输入源 | +| 手势识别器独立于 TouchHandler | 解耦触摸处理与手势判定,可单独禁用/替换手势引擎 | 将手势检测逻辑内嵌在 TouchHandler 中 | +| 空间距离焦点导航算法 | 工控屏布局不规则,Tab 顺序不够直观;空间距离更符合用户预期 | 仅使用 Qt Tab 顺序 | +| 原生设备层 (evdev/GPIO/RotaryEncoder) 直接读取 | linuxfb/eglfs 平台下 Qt 不一定转发所有输入;直接读取保证可靠性 | 完全依赖 Qt 平台插件转发 | +| 模拟器输入映射 (鼠标->触摸, 滚轮->旋钮) | 开发机没有触摸/旋钮硬件,映射确保桌面端可调试嵌入式行为 | 要求必须有触摸硬件才能开发 | -## 十四、下一步行动 +## 当前状态 -完成 Phase 3 后,进入 **Phase 4: Shell UI 主体**。 +未实现。参见 [status/current.md](../status/current.md) Phase 3 部分。 diff --git a/document/design_stage/04_phase6_simulator.md b/document/design_stage/04_phase6_simulator.md index d14c87bfe..1c8614eea 100644 --- a/document/design_stage/04_phase6_simulator.md +++ b/document/design_stage/04_phase6_simulator.md @@ -1,1020 +1,28 @@ --- title: "Phase 6: 多平台模拟器详细设计文档" -description: "完成 Phase 6 后,进入 Phase 4: Shell UI 主体 或 Phase 5: SD" +description: "在桌面端还原嵌入式设备 UI 效果,实现所见即所得开发体验" --- -# Phase 6: 多平台模拟器详细设计文档 +# Phase 6: 多平台模拟器 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 6 - 多平台模拟器 | -| 预计周期 | 2~3 周 | -| 依赖阶段 | Phase 0, Phase 2, Phase 3 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -在 Windows/Ubuntu 上通过 Qt Creator 还原真实嵌入式设备的 UI 效果,实现"所见即所得"的开发体验。 - -### 1.2 具体交付物 -- [ ] `SimulatorWindow` 模拟器主窗口 -- [ ] `DeviceFrame` 设备外壳渲染 -- [ ] `TouchVisualizer` 触摸可视化 -- [ ] `HWTierSelector` 硬件档位选择器 -- [ ] `ResolutionPreset` 分辨率预设管理 -- [ ] 独立可执行文件 `cfdesktop-sim` - ---- - -## 二、模块架构设计 - -### 2.1 整体架构图 - -```text -┌───────────────────────────────────────────────────────────────┐ -│ Simulator Window │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Device Frame │ │ -│ │ ┌───────────────────────────────────────────────────┐ │ │ -│ │ │ Shell UI Content │ │ │ -│ │ │ (共享真实设备的 Shell 代码) │ │ │ -│ │ │ │ │ │ -│ │ │ [Launcher] [WindowManager] [StatusBar] │ │ │ -│ │ └───────────────────────────────────────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Control Panel │ │ -│ │ [Device Selector] [Resolution] [HWTier] [Touch Viz] │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ Injection Layer │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ -│ │ DPIManager │ │HardwareProbe │ │ InputManager │ │ -│ │ Injection │ │ Mock │ │ Simulation │ │ -│ └──────────────┘ └──────────────┘ └──────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────┐ -│ Shared Base & Shell │ -│ (与真实设备完全相同的代码) │ -└───────────────────────────────────────────────────────────────┘ -```text - -### 2.2 文件结构 - -```text -src/simulator/ -├── include/CFDesktop/Simulator/ -│ ├── SimulatorWindow.h # 模拟器主窗口 -│ ├── DeviceFrame.h # 设备外壳 -│ ├── DeviceProfile.h # 设备配置文件 -│ ├── TouchVisualizer.h # 触摸可视化 -│ ├── ControlPanel.h # 控制面板 -│ ├── HWTierSelector.h # 硬件档位选择器 -│ ├── ResolutionPreset.h # 分辨率预设 -│ └── SimulatorInjection.h # 注入接口 -│ -└── src/ - ├── SimulatorWindow.cpp - ├── DeviceFrame.cpp - ├── TouchVisualizer.cpp - ├── ControlPanel.cpp - ├── HWTierSelector.cpp - ├── ResolutionPreset.cpp - └── injection/ - ├── DPIInjector.cpp # DPI 注入 - ├── HardwareMock.cpp # 硬件 Mock - └── InputSimulator.cpp # 输入模拟 - -assets/simulator/ -├── devices/ # 设备外壳图片 -│ ├── phone-generic.png -│ ├── tablet-10inch.png -│ ├── panel-7inch.png -│ ├── panel-10inch.png -│ └── custom/ -│ └── device.svg -├── profiles/ # 设备配置 -│ ├── imx6ull-4.3.json -│ ├── imx6ull-7.0.json -│ ├── rk3568-7.0.json -│ ├── rk3568-10.1.json -│ ├── rk3588-10.1.json -│ └── generic-1080p.json -└── effects/ - └── touch-ripple.png # 触摸涟漪效果 -```yaml - ---- - -## 三、模拟器主窗口 (SimulatorWindow) - -### 3.1 SimulatorWindow 类接口 - -**文件**: `include/CFDesktop/Simulator/SimulatorWindow.h` - -```cpp -#pragma once -#include -#include -#include "DeviceProfile.h" - -namespace CFDesktop::Simulator { - -class DeviceFrame; -class ControlPanel; -class TouchVisualizer; -class DPIInjector; -class HardwareMock; - -/** - * @brief 模拟器主窗口 - * - * 提供完整的嵌入式设备模拟环境。 - */ -class SimulatorWindow : public QMainWindow { - Q_OBJECT - -public: - explicit SimulatorWindow(QWidget* parent = nullptr); - ~SimulatorWindow(); - - /** - * @brief 加载设备配置 - */ - bool loadDeviceProfile(const QString& profilePath); - - /** - * @brief 获取当前设备配置 - */ - DeviceProfile currentProfile() const; - - /** - * @brief 设置显示内容 - * - * 通常设置为 Shell UI 的主窗口。 - */ - void setContentWidget(QWidget* content); - - /** - * @brief 获取内容区域 - */ - QWidget* contentWidget() const; - - /** - * @brief 显示/隐藏控制面板 - */ - void setControlPanelVisible(bool visible); - bool isControlPanelVisible() const; - - /** - * @brief 显示/隐藏设备外壳 - */ - void setDeviceFrameVisible(bool visible); - bool isDeviceFrameVisible() const; - - /** - * @brief 启用/禁用触摸可视化 - */ - void setTouchVisualizationEnabled(bool enabled); - bool isTouchVisualizationEnabled() const; - -public slots: - /** - * @brief 切换全屏模式 - */ - void toggleFullscreen(); - - /** - * @brief 重置模拟状态 - */ - void resetSimulation(); - - /** - * @brief 截图 - */ - void takeScreenshot(); - -signals: - /** - * @brief 设备配置变化信号 - */ - void profileChanged(const DeviceProfile& profile); - - /** - * @brief 分辨率变化信号 - */ - void resolutionChanged(const QSize& resolution); - - /** - * @brief 硬件档位变化信号 - */ - void hwTierChanged(HWTier tier); - -private: - void setupUI(); - void setupConnections(); - void loadDefaultProfile(); - -private: - QScopedPointer m_deviceFrame; - QScopedPointer m_controlPanel; - QScopedPointer m_touchVisualizer; - QScopedPointer m_dpiInjector; - QScopedPointer m_hardwareMock; - - DeviceProfile m_currentProfile; - QPointer m_contentWidget; -}; - -} // namespace CFDesktop::Simulator -```text - -### 3.2 窗口布局 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Simulator Window │ -├─────────────────────────────────────────────────────────────┤ -│ ┌───┐ │ -│ │ │ ┌───────────────────────────────────────────────┐ │ -│ │ D │ │ │ │ -│ │ e │ │ Device Frame (外壳) │ │ -│ │ v │ │ ┌─────────────────────────────────────────┐ │ │ -│ │ i │ │ │ │ │ │ -│ │ c │ │ │ Screen Content Area │ │ │ -│ │ e │ │ │ (Shell UI 渲染) │ │ │ -│ │ │ │ │ │ │ │ -│ │ F │ │ │ │ │ │ -│ │ r │ │ │ │ │ │ -│ │ a │ │ │ │ │ │ -│ │ m │ │ │ │ │ │ -│ │ e │ │ └─────────────────────────────────────────┘ │ │ -│ └───┘ │ │ │ -│ └───────────────────────────────────────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Control Panel │ │ -│ │ [Device▼] [800×480▼] [Low▼] [Touch:ON] [Screenshot] │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -```yaml - ---- - -## 四、设备配置文件 (DeviceProfile) - -### 4.1 DeviceProfile 数据结构 - -**文件**: `include/CFDesktop/Simulator/DeviceProfile.h` - -```cpp -#pragma once -#include -#include -#include -#include "../Base/HardwareProbe/HWTier.h" - -namespace CFDesktop::Simulator { - -/** - * @brief 设备类型 - */ -enum class DeviceType { - Phone, # 手机 - Tablet, # 平板 - Panel, # 工控屏 - Custom # 自定义 -}; - -/** - * @brief 设备配置 - */ -struct DeviceProfile { - // 基本信息 - QString name; # 设备名称 - QString manufacturer; # 制造商 - QString model; # 型号 - DeviceType type = DeviceType::Panel; - - // 屏幕参数 - QSize screenSize; # 屏幕分辨率 - QSizeF physicalSize; # 物理尺寸 (毫米) - qreal dpi = 96.0; # DPI - qreal devicePixelRatio = 1.0; # 设备像素比 - - // 屏幕区域 (在外壳中的位置和大小) - QRect screenRect; # 屏幕区域 - - // 设备外壳 - QString frameImagePath; # 外壳图片路径 - QRect frameRect; # 外壳区域 - QColor frameColor; # 外壳颜色 (无图片时) - int frameCornerRadius = 0; # 外壳圆角 - - // 硬件能力 - HWTier hwTier = HWTier::Low; # 硬件档位 - - // 输入配置 - bool hasTouchscreen = true; - bool hasHardwareButtons = false; - bool hasRotaryEncoder = false; - - // 其他配置 - QString description; - QString version = "1.0"; - - /** - * @brief 从 JSON 加载 - */ - static DeviceProfile fromJson(const QString& path); - - /** - * @brief 保存到 JSON - */ - bool toJson(const QString& path) const; - - /** - * @brief 获取默认配置 - */ - static DeviceProfile defaultProfile(); -}; - -} // namespace CFDesktop::Simulator -```text - -### 4.2 设备配置 JSON 示例 - -**文件**: `assets/simulator/profiles/imx6ull-4.3.json` - -```json -{ - "name": "IMX6ULL 4.3 inch Panel", - "manufacturer": "NXP", - "model": "IMX6ULL-EVK", - "type": "panel", - - "screen": { - "size": { "width": 480, "height": 272 }, - "physicalSize": { "width": 95.0, "height": 54.0 }, - "dpi": 125.0, - "devicePixelRatio": 1.0, - "rect": { "x": 20, "y": 20, "width": 480, "height": 272 } - }, - - "frame": { - "image": "devices/panel-4.3.png", - "rect": { "x": 0, "y": 0, "width": 520, "height": 312 }, - "color": "#333333", - "cornerRadius": 8 - }, - - "hardware": { - "tier": "low" - }, - - "input": { - "touchscreen": true, - "hardwareButtons": true, - "rotaryEncoder": false - }, - - "description": "4.3 inch WVGA panel with IMX6ULL", - "version": "1.0" -} -```bash - -### 4.3 预设设备配置列表 - -| 配置文件 | 设备 | 分辨率 | DPI | 档位 | -|----------|------|--------|-----|------| -| `imx6ull-4.3.json` | 4.3寸工控屏 | 480×272 | 125 | Low | -| `imx6ull-7.0.json` | 7.0寸工控屏 | 800×480 | 133 | Low | -| `rk3568-7.0.json` | 7.0寸工控屏 | 1024×600 | 170 | Mid | -| `rk3568-10.1.json` | 10.1寸平板 | 1280×800 | 149 | Mid | -| `rk3588-10.1.json` | 10.1寸高端屏 | 1920×1200 | 224 | High | - ---- - -## 五、设备外壳 (DeviceFrame) - -### 5.1 DeviceFrame 类接口 - -**文件**: `include/CFDesktop/Simulator/DeviceFrame.h` - -```cpp -#pragma once -#include -#include "DeviceProfile.h" - -namespace CFDesktop::Simulator { - -/** - * @brief 设备外壳渲染 - * - * 渲染设备外壳和屏幕区域。 - */ -class DeviceFrame : public QWidget { - Q_OBJECT - -public: - explicit DeviceFrame(QWidget* parent = nullptr); - ~DeviceFrame() = default; - - /** - * @brief 设置设备配置 - */ - void setDeviceProfile(const DeviceProfile& profile); - DeviceProfile deviceProfile() const; - - /** - * @brief 获取屏幕区域 Widget - * - * Shell UI 内容应该作为这个 Widget 的子控件。 - */ - QWidget* screenContainer() const; - - /** - * @brief 绘制模式 - */ - enum class RenderMode { - Image, # 使用图片 - Vector, # 矢量绘制 - Simple # 简单边框 - }; - void setRenderMode(RenderMode mode); - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - void drawFrameImage(QPainter& painter); - void drawFrameVector(QPainter& painter); - void drawFrameSimple(QPainter& painter); - void updateLayout(); - -private: - DeviceProfile m_profile; - RenderMode m_renderMode = RenderMode::Image; - QPixmap m_frameImage; - QPointer m_screenContainer; -}; - -} // namespace CFDesktop::Simulator -```text - -### 5.2 矢量绘制示例 - -```cpp -void DeviceFrame::drawFrameVector(QPainter& painter) { - painter.setRenderHint(QPainter::Antialiasing); - - // 外壳背景 - QPainterPath framePath; - framePath.addRoundedRect( - m_profile.frameRect, - m_profile.frameCornerRadius, - m_profile.frameCornerRadius - ); - - painter.fillPath(framePath, m_profile.frameColor); - - // 屏幕边框 - QPen borderPen(QColor(0, 0, 0, 100), 2); - painter.setPen(borderPen); - painter.drawRoundedRect( - m_profile.screenRect.adjusted(-2, -2, 2, 2), - 4, 4 - ); - - // 屏幕阴影 - QRect screenShadowRect = m_profile.screenRect; - screenShadowRect.translate(2, 2); - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 0, 0, 50)); - painter.drawRoundedRect(screenShadowRect, 4, 4); - - // 硬件按键 - if (m_profile.hasHardwareButtons) { - drawHardwareButtons(painter); - } -} -```yaml - ---- - -## 六、触摸可视化 (TouchVisualizer) - -### 6.1 TouchVisualizer 类接口 - -**文件**: `include/CFDesktop/Simulator/TouchVisualizer.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 触摸点可视化数据 - */ -struct TouchPointVisual { - int id; - QPointF position; - qreal pressure = 0.0; - qint64 startTime = 0; - qreal radius = 20.0; - QColor color = QColor(0, 150, 255, 150); -}; - -/** - * @brief 触摸可视化 - * - * 在点击位置显示涟漪效果。 - */ -class TouchVisualizer : public QWidget { - Q_OBJECT - -public: - explicit TouchVisualizer(QWidget* parent = nullptr); - ~TouchVisualizer() = default; - - /** - * @brief 启用/禁用 - */ - void setEnabled(bool enabled); - bool isEnabled() const; - - /** - * @brief 添加触摸点 - */ - void addTouchPoint(int id, const QPointF& position); - - /** - * @brief 更新触摸点 - */ - void updateTouchPoint(int id, const QPointF& position); - - /** - * @brief 移除触摸点 - */ - void removeTouchPoint(int id); - - /** - * @brief 设置涟漪效果时长 - */ - void setRippleDuration(int milliseconds); +CFDesktop 的目标设备包括 IMX6ULL (480x272)、RK3568 (1024x600)、RK3588 (1920x1200) 等差异极大的嵌入式屏幕。开发者日常在 Windows/Ubuntu 桌面机上工作,如果没有模拟器,每次 UI 调整都必须烧录固件到真机验证,迭代周期从秒级拉长到分钟级。 - /** - * @brief 设置涟漪颜色 - */ - void setRippleColor(const QColor& color); +核心思路是 **DeviceFrame + 注入层**:模拟器窗口由 `DeviceFrame`(设备外壳渲染,提供真实屏幕边框和尺寸感)包裹 Shell UI 内容,下方 `ControlPanel` 提供设备/分辨率/档位切换。关键在于 Shell UI 代码与真机完全共享——模拟器不做任何 UI 级别的模拟,而是通过注入层(DPIInjector / HardwareMock / InputSimulator)将模拟的屏幕参数、硬件信息、输入事件注入到 base 层。这样模拟器中看到的 UI 效果与真机一致,且切换设备配置时 DPI、主题降级、动画开关等行为即时生效。 -protected: - void paintEvent(QPaintEvent* event) override; +触摸可视化(TouchVisualizer)通过涟漪动画显示触摸点位置和轨迹,帮助开发者在鼠标模拟触摸时直观确认输入行为。设备配置以 JSON 文件描述(DeviceProfile),预设覆盖主流嵌入式屏幕,也可自定义扩展。 -private: - void startRippleAnimation(const QPointF& position); - void cleanupExpiredTouches(); +## 关键决策 -private: - bool m_enabled = true; - int m_rippleDuration = 300; - QColor m_ippleColor; - QHash m_activeTouches; - QList> m_activeAnimations; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 七、控制面板 (ControlPanel) - -### 7.1 ControlPanel 类接口 - -**文件**: `include/CFDesktop/Simulator/ControlPanel.h` - -```cpp -#pragma once -#include -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 模拟器控制面板 - * - * 提供设备选择、分辨率切换、档位切换等功能。 - */ -class ControlPanel : public QWidget { - Q_OBJECT - -public: - explicit ControlPanel(QWidget* parent = nullptr); - ~ControlPanel() = default; - - /** - * @brief 设置可用设备配置列表 - */ - void setAvailableProfiles(const QList& profiles); - - /** - * @brief 设置当前配置 - */ - void setCurrentProfile(const DeviceProfile& profile); - - /** - * @brief 设置当前分辨率 - */ - void setResolution(const QSize& resolution); - - /** - * @brief 设置当前硬件档位 - */ - void setHWTier(HWTier tier); - -signals: - /** - * @brief 设备配置变化 - */ - void profileChanged(const DeviceProfile& profile); - - /** - * @brief 分辨率变化 - */ - void resolutionChanged(const QSize& resolution); - - /** - * @brief 硬件档位变化 - */ - void hwTierChanged(HWTier tier); - - /** - * @brief 触摸可视化开关变化 - */ - void touchVisualizationChanged(bool enabled); - - /** - * @brief 请求截图 - */ - void screenshotRequested(); - - /** - * @brief 请求重置 - */ - void resetRequested(); - - /** - * @brief 请求全屏 - */ - void fullscreenRequested(); - -private: - void setupUI(); - void loadProfilesFromDirectory(); - -private: - QComboBox* m_profileCombo; - QComboBox* m_resolutionCombo; - QComboBox* m_hwTierCombo; - QCheckBox* m_touchVizCheck; - QPushButton* m_screenshotButton; - QPushButton* m_resetButton; - QPushButton* m_fullscreenButton; - - QList m_availableProfiles; -}; - -} // namespace CFDesktop::Simulator -```text - -### 7.2 硬件档位选择器 (HWTierSelector) - -**文件**: `include/CFDesktop/Simulator/HWTierSelector.h` - -```cpp -#pragma once -#include -#include -#include - -namespace CFDesktop::Simulator { - -/** - * @brief 硬件档位选择器 - * - * 允许在模拟器中切换硬件档位,验证降级行为。 - */ -class HWTierSelector : public QWidget { - Q_OBJECT - -public: - explicit HWTierSelector(QWidget* parent = nullptr); - ~HWTierSelector() = default; - - void setCurrentTier(HWTier tier); - HWTier currentTier() const; - -signals: - void tierChanged(HWTier tier); - -private: - QButtonGroup* m_tierGroup; - QRadioButton* m_lowRadio; - QRadioButton* m_midRadio; - QRadioButton* m_highRadio; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 八、注入接口 - -### 8.1 DPI 注入器 - -**文件**: `src/simulator/injection/DPIInjector.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief DPI 注入器 - * - * 向 DPIManager 注入模拟的屏幕参数。 - */ -class DPIInjector : public QObject { - Q_OBJECT - -public: - explicit DPIInjector(QObject* parent = nullptr); - - /** - * @brief 注入屏幕参数 - */ - void inject( - const QSize& resolution, - qreal dpi, - qreal devicePixelRatio - ); - - /** - * @brief 清除注入 - */ - void clear(); - - /** - * @brief 是否已注入 - */ - bool isInjected() const; - -private: - bool m_injected = false; -}; - -} // namespace CFDesktop::Simulator -```text - -### 8.2 硬件 Mock - -**文件**: `src/simulator/injection/HardwareMock.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief 硬件 Mock - * - * 向 HardwareProbe 注入模拟的硬件信息。 - */ -class HardwareMock : public QObject { - Q_OBJECT - -public: - explicit HardwareMock(QObject* parent = nullptr); - - /** - * @brief 注入硬件信息 - */ - void injectHardwareInfo(const HardwareInfo& info); - - /** - * @brief 快速注入档位 - */ - void injectTier(HWTier tier); - - /** - * @brief 清除 Mock - */ - void clear(); - -signals: - /** - * @brief 档位变化信号 - */ - void tierChanged(HWTier tier); - -private: - bool m_active = false; -}; - -} // namespace CFDesktop::Simulator -```text - -### 8.3 输入模拟器 - -**文件**: `src/simulator/injection/InputSimulator.cpp` - -```cpp -namespace CFDesktop::Simulator { - -/** - * @brief 输入模拟器配置 - */ -struct InputSimulationConfig { - bool mouseEmulatesTouch = true; - bool keyboardEmulatesButtons = true; - bool mouseWheelEmulatesRotary = true; - bool showTouchFeedback = true; -}; - -/** - * @brief 输入模拟器 - * - * 将鼠标/键盘输入转换为触摸/按键事件。 - */ -class InputSimulator : public QObject { - Q_OBJECT - -public: - explicit InputSimulator(QObject* parent = nullptr); - - void setConfig(const InputSimulationConfig& config); - InputSimulationConfig config() const; - - void setEnabled(bool enabled); - -protected: - bool eventFilter(QObject* watched, QEvent* event) override; - -private: - bool handleMouseEvent(QMouseEvent* event); - bool handleWheelEvent(QWheelEvent* event); - bool handleKeyEvent(QKeyEvent* event); - PointerEvent* createTouchEvent(QMouseEvent* event); - -private: - InputSimulationConfig m_config; - bool m_enabled = true; - QPointF m_lastTouchPosition; -}; - -} // namespace CFDesktop::Simulator -```yaml - ---- - -## 九、详细任务清单 - -### 9.1 Week 1: 基础框架 - -#### Day 1-2: 主窗口与设备外壳 -- [ ] 创建 SimulatorWindow 类 -- [ ] 实现 DeviceFrame 基础绘制 -- [ ] 实现矢量外壳绘制 -- [ ] 支持图片外壳 - -#### Day 3: 设备配置 -- [ ] 定义 DeviceProfile 结构 -- [ ] 实现 JSON 序列化 -- [ ] 创建预设配置文件 -- [ ] 实现配置加载 - -#### Day 4: 控制面板 -- [ ] 创建 ControlPanel 类 -- [ ] 实现设备选择器 -- [ ] 实现分辨率选择器 -- [ ] 实现档位选择器 - -#### Day 5: 集成测试 -- [ ] 集成各模块 -- [ ] 基本功能测试 - -### 9.2 Week 2: 注入与模拟 - -#### Day 1-2: DPI 注入 -- [ ] 实现 DPI 注入器 -- [ ] 修改 DPIManager 支持注入 -- [ ] 测试不同分辨率 - -#### Day 3: 硬件 Mock -- [ ] 实现硬件信息 Mock -- [ ] 修改 HardwareProbe 支持 Mock -- [ ] 测试档位切换 - -#### Day 4: 输入模拟 -- [ ] 实现输入模拟器 -- [ ] 鼠标转触摸 -- [ ] 键盘转按键 - -#### Day 5: 触摸可视化 -- [ ] 实现 TouchVisualizer -- [ ] 涟漪动画 -- [ ] 多点触摸支持 - -### 9.3 Week 3: 完善与优化 - -#### Day 1-2: UI 完善 -- [ ] 完善设备外壳 -- [ ] 添加更多预设 -- [ ] 美化控制面板 - -#### Day 3: 高级功能 -- [ ] 截图功能 -- [ ] 录屏功能 -- [ ] 性能监控 - -#### Day 4: 文档 -- [ ] 使用说明 -- [ ] API 文档 -- [ ] 示例代码 - -#### Day 5: 测试 -- [ ] 跨平台测试 -- [ ] 性能测试 -- [ ] 修复 Bug - ---- - -## 十、验收标准 - -### 10.1 功能验收 -- [ ] 能正确显示各种分辨率 -- [ ] 档位切换生效 -- [ ] 触摸模拟正常 -- [ ] 截图功能正常 - -### 10.2 性能验收 -- [ ] 启动时间 < 2 秒 -- [ ] 帧率稳定 60fps - -### 10.3 兼容性验收 -- [ ] Windows 10/11 正常运行 -- [ ] Ubuntu 20.04+ 正常运行 -- [ ] macOS 12+ 正常运行 - ---- - -## 十一、使用示例 - -### 11.1 启动模拟器 - -```cpp -int main(int argc, char* argv[]) { - QApplication app(argc, argv); - - // 创建模拟器窗口 - SimulatorWindow simulator; - simulator.show(); - - // 创建 Shell UI - ShellWindow* shell = new ShellWindow(); - simulator.setContentWidget(shell); - - return app.exec(); -} -```text - -### 11.2 切换设备配置 - -```cpp -// 加载新配置 -simulator.loadDeviceProfile("/path/to/imx6ull-7.0.json"); - -// 或者通过控制面板选择 -simulator.controlPanel()->setCurrentProfile("IMX6ULL 7.0 inch"); -```yaml - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| DeviceFrame 包裹式架构(共享 Shell UI 代码) | 模拟器与真机跑同一份 UI 代码,保证一致性,避免两套代码漂移 | 模拟器独立渲染一套简化 UI | +| 注入层 (DPIInjector/HardwareMock/InputSimulator) | 通过注入模拟参数让 base 层"以为"自己在真机上运行,不侵入业务代码 | 在 Shell UI 层到处加 `#ifdef SIMULATOR` 条件编译 | +| JSON 设备配置文件 (DeviceProfile) | 设备参数可热加载、用户可自定义新增设备,无需重编译 | 硬编码设备参数 | +| ControlPanel 运行时切换设备/档位 | 无需重启即可验证不同分辨率和硬件档位下的 UI 表现 | 每次切换需重启模拟器 | +| 触摸涟漪可视化 | 鼠标模拟触摸时没有触觉反馈,涟漪帮助确认点击位置和手势轨迹 | 不做可视化,依赖日志判断 | -## 十二、下一步行动 +## 当前状态 -完成 Phase 6 后,进入 **Phase 4: Shell UI 主体** 或 **Phase 5: SDK 导出层**。 +未实现。参见 [status/current.md](../status/current.md) Phase 6 部分。 diff --git a/document/design_stage/05_phase8_testing.md b/document/design_stage/05_phase8_testing.md index ec488100f..15486dd17 100644 --- a/document/design_stage/05_phase8_testing.md +++ b/document/design_stage/05_phase8_testing.md @@ -1,912 +1,25 @@ --- title: "Phase 8: 测试体系详细设计文档" -description: "Phase 8: 测试体系详细设计文档 的详细文档" +description: "建立单元测试、集成测试、UI 自动化测试和跨平台 CI 的完整测试体系" --- -# Phase 8: 测试体系详细设计文档 +# Phase 8: 测试体系 -- 设计意图 -## 文档信息 -| 项目 | 内容 | -|------|------| -| 文档版本 | v1.0 | -| 创建日期 | 2026-02-20 | -| 阶段代号 | Phase 8 - 测试体系 | -| 预计周期 | 贯穿全程 | -| 依赖阶段 | 所有阶段 | +## 为什么选择这种方案 ---- - -## 一、阶段目标 - -### 1.1 核心目标 -建立完整的测试体系,确保代码质量和系统稳定性。测试体系覆盖单元测试、集成测试、UI 自动化测试和跨平台 CI。 - -### 1.2 具体交付物 -- [ ] 单元测试框架和测试用例 -- [ ] 集成测试套件 -- [ ] UI 自动化测试 -- [ ] CI/CD 流水线配置 -- [ ] 测试覆盖率报告 -- [ ] 性能测试基准 - ---- - -## 二、测试体系架构 - -### 2.1 测试金字塔 - -```text - /\ - / \ - / E2E \ - / 测试 \ - /________\ - / \ - / \ - / UI 自动化 \ - / 测试 \ - /__________________\ - / \ - / 集成测试 \ - /________________________\ - / \ - / 单元测试 \ - /______________________________\ - / 70% 单元 \ - / 20% 集成 \ - / 10% UI \ - /____________________________________\ -```text - -### 2.2 测试目录结构 - -```text -tests/ -├── unit/ # 单元测试 -│ ├── base/ -│ │ ├── test_hardware_probe.cpp -│ │ ├── test_theme_engine.cpp -│ │ ├── test_animation_manager.cpp -│ │ ├── test_dpi_manager.cpp -│ │ ├── test_config_store.cpp -│ │ └── test_logger.cpp -│ │ -│ ├── sdk/ -│ │ ├── test_widgets.cpp -│ │ ├── test_system_api.cpp -│ │ └── test_app_lifecycle.cpp -│ │ -│ └── shell/ -│ ├── test_launcher.cpp -│ ├── test_window_manager.cpp -│ └── test_status_bar.cpp -│ -├── integration/ # 集成测试 -│ ├── test_base_integration.cpp -│ ├── test_sdk_integration.cpp -│ └── test_shell_integration.cpp -│ -├── ui/ # UI 自动化测试 -│ ├── test_launcher_ui.cpp -│ ├── test_window_manager_ui.cpp -│ └── test_app_switching.cpp -│ -├── performance/ # 性能测试 -│ ├── test_startup_time.cpp -│ ├── test_animation_fps.cpp -│ ├── test_memory_usage.cpp -│ └── benchmarks/ -│ ├── benchmark_theme_load.cpp -│ └── benchmark_input_processing.cpp -│ -├── mock/ # Mock 数据 -│ ├── proc/ -│ │ ├── cpuinfo_imx6ull -│ │ ├── cpuinfo_rk3568 -│ │ ├── meminfo_512mb -│ │ └── meminfo_1gb -│ └── devices/ -│ └── dri/ -│ └── card0 -│ -├── fixtures/ # 测试夹具 -│ ├── test_app.h -│ ├── test_widget.h -│ └── test_theme.h -│ -└── CMakeLists.txt -```yaml - ---- - -## 三、单元测试 - -### 3.1 测试框架配置 - -**文件**: `tests/CMakeLists.txt` - -```cmake -# 启用测试 -enable_testing() - -# Qt6 Test 包 -find_package(Qt6 REQUIRED COMPONENTS Test) - -# 添加测试子目录 -add_subdirectory(unit) -add_subdirectory(integration) -add_subdirectory(ui) -add_subdirectory(performance) -```text - -### 3.2 基础测试模板 - -```cpp -// tests/unit/base/test_hardware_probe.cpp - -#include -#include - -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); // 测试套件开始前执行一次 - void cleanupTestCase(); // 测试套件结束后执行一次 - void init(); // 每个测试用例前执行 - void cleanup(); // 每个测试用例后执行 - - // CPU 检测测试 - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - void testDetectCPU_X86_64(); - - // 档位计算测试 - void testCalculateTier_LowEnd(); - void testCalculateTier_MidRange(); - void testCalculateTier_HighEnd(); - - // Mock 数据测试 - void testWithMockData(); - void testWithInvalidData(); -}; - -void TestHardwareProbe::initTestCase() { - // 设置测试环境 - qputenv("CFDESKTOP_TEST_MODE", "1"); -} - -void TestHardwareProbe::testDetectCPU_IMX6ULL() { - // 设置 Mock 数据路径 - qputenv("CFDESKTOP_MOCK_CPUINFO", - "/path/to/mock/proc/cpuinfo_imx6ull"); - - HardwareProbe probe; - HardwareInfo info = probe.probe(); - - // 验证 CPU 信息 - QCOMPARE(info.cpu.cores, 1); - QCOMPARE(info.cpu.architecture, QString("armv7l")); - QVERIFY(info.cpu.features.contains("neon")); - - // 验证档位 - QCOMPARE(info.tier, HWTier::Low); -} - -QTEST_MAIN(TestHardwareProbe) -#include "test_hardware_probe.moc" -```text - -### 3.3 Mock 系统设计 - -```cpp -// tests/fixtures/test_helpers.h - -namespace CFDesktop::Testing { - -/** - * @brief Mock 系统调用 - */ -class MockSystem { -public: - /** - * @brief Mock /proc/cpuinfo - */ - static void mockCpuinfo(const QString& mockFile); - - /** - * @brief Mock /proc/meminfo - */ - static void mockMeminfo(const QString& mockFile); - - /** - * @brief Mock /sys/class/net/* - */ - static void mockNetwork(const QList& interfaces); - - /** - * @brief 清除所有 Mock - */ - static void clearAll(); - - /** - * @brief 设置模拟屏幕参数 - */ - static void mockScreen(int dpi, qreal dpr, const QSize& size); -}; - -} // namespace CFDesktop::Testing -```bash - -### 3.4 关键单元测试用例清单 - -#### HardwareProbe 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testDetectCPU_IMX6ULL` | IMX6ULL CPU 检测 | 识别为 Low 档 | -| `testDetectCPU_RK3568` | RK3568 CPU 检测 | 识别为 Mid 档 | -| `testDetectCPU_RK3588` | RK3588 CPU 检测 | 识别为 High 档 | -| `testDetectGPU_WithDRM` | DRM 设备检测 | 正确检测 GPU | -| `testDetectGPU_NoDRM` | 无 DRM 设备 | 返回无 GPU | -| `testDetectMemory_512MB` | 512MB 内存检测 | 正确报告内存 | -| `testCalculateTier_ScoreBased` | 评分计算 | 档位正确 | -| `testUserOverride` | 用户配置覆盖 | 使用用户档位 | -| `testCustomScript` | 自定义脚本执行 | 脚本结果生效 | - -#### ThemeEngine 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testLoadDefaultTheme` | 加载默认主题 | 成功加载 | -| `testThemeVariableAccess` | 变量访问 | 返回正确值 | -| `testThemeInheritance` | 主题继承 | 继承生效 | -| `testQSSProcessing` | QSS 变量替换 | 变量被替换 | -| `testThemeHotReload` | 热重载 | 实时生效 | -| `testTierBasedFallback` | 档位降级 | Low 档禁用特效 | - -#### AnimationManager 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testFadeInAnimation` | 淡入动画 | 正常播放 | -| `testLowTierNoAnimation` | Low 档 | 动画时长为 0 | -| `testParallelGroup` | 并行动画组 | 同时播放 | -| `testSequentialGroup` | 串行动画组 | 顺序播放 | -| `testAnimationLifecycle` | 动画生命周期 | 状态正确 | - -#### DPIManager 测试用例 - -| 测试用例 | 描述 | 预期结果 | -|----------|------|----------| -| `testDPConversion` | dp 转 px | 转换正确 | -| `testSPConversion` | sp 转 px | 考虑字体缩放 | -| `testInjection` | 参数注入 | 注入值生效 | -| `testHighDPI` | 高 DPI | 缩放正确 | - ---- - -## 四、集成测试 - -### 4.1 集成测试框架 - -```cpp -// tests/integration/test_base_integration.cpp - -#include -#include -#include -#include - -class TestBaseIntegration : public QObject { - Q_OBJECT - -private slots: - void testHardwareProbeToTheme(); - void testHardwareProbeToAnimation(); - void testThemeToWidget(); - void testFullBaseStack(); -}; - -void TestBaseIntegration::testHardwareProbeToTheme() { - // 1. 硬件检测 - HardwareProbe probe; - probe.setMockData(createLowEndMockInfo()); - HardwareInfo info = probe.probe(); - - // 2. 主题引擎应响应档位 - ThemeEngine* theme = ThemeEngine::instance(); - theme->loadTheme("/path/to/default/theme"); - QColor shadowColor = theme->color("shadow"); - - // Low 档应该禁用阴影 (透明或无色) - if (info.tier == HWTier::Low) { - QVERIFY(!shadowColor.alpha() > 0); - } -} - -void TestBaseIntegration::testFullBaseStack() { - // 完整的 Base 库集成测试 - // 模拟真实启动流程 -} -```text - -### 4.2 测试应用框架 - -```cpp -// tests/fixtures/test_app.h - -namespace CFDesktop::Testing { - -/** - * @brief 测试用应用程序 - * - * 提供独立的测试环境,不影响主应用。 - */ -class TestApplication : public QApplication { -public: - TestApplication(int& argc, char** argv); - - /** - * @brief 使用测试配置初始化 - */ - void initializeWithTestConfig(); - - /** - * @brief 创建测试窗口 - */ - QWidget* createTestWindow(); - - /** - * @brief 模拟用户输入 - */ - void simulateKeyPress(int key); - void simulateMouseClick(const QPoint& pos); - void simulateTouch(const QPointF& pos); - - /** - * @brief 等待条件满足 - */ - bool waitFor(std::function condition, int timeout = 5000); - -private: - void setupTestEnvironment(); - void cleanupTestEnvironment(); -}; - -} // namespace CFDesktop::Testing -```yaml - ---- - -## 五、UI 自动化测试 - -### 5.1 QTest UI 测试 - -```cpp -// tests/ui/test_launcher_ui.cpp - -#include -#include - -class TestLauncherUI : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); - void cleanupTestCase(); - - void testLauncherDisplay(); - void testAppIconClick(); - void testSwipeGesture(); - void testAppLaunch(); - -private: - LauncherWindow* m_launcher; -}; - -void TestLauncherUI::testAppIconClick() { - // 查找应用图标 - QList icons = m_launcher->findChildren(); - QVERIFY(icons.size() > 0); - - AppIconWidget* firstIcon = icons.first(); - - // 模拟点击 - QTest::mouseClick(firstIcon, Qt::LeftButton); - - // 验证应用启动 - QSignalSpy spy(firstIcon, &AppIconWidget::launched); - QVERIFY(spy.wait(1000)); -} - -void TestLauncherUI::testSwipeGesture() { - // 模拟滑动手势 - QWidget* viewport = m_launcher->viewport(); - - QPoint startPos(400, 240); - QPoint endPos(100, 240); - - QTest::mousePress(viewport, Qt::LeftButton, startPos); - QTest::mouseMove(viewport, endPos); - QTest::mouseRelease(viewport, Qt::LeftButton, endPos); - - // 验证页面切换 - QTest::qWait(500); - QCOMPARE(m_launcher->currentPage(), 1); -} -```text - -### 5.2 可访问性测试 - -```cpp -void TestAccessibility::testWidgetNavigation() { - // 验证焦点导航 - QWidget* widget1 = m_window->findChild("widget1"); - QWidget* widget2 = m_window->findChild("widget2"); - - widget1->setFocus(); - QVERIFY(widget1->hasFocus()); - - // 模拟 Tab 键 - QTest::keyClick(m_window, Qt::Key_Tab); - QVERIFY(widget2->hasFocus()); -} - -void TestAccessibility::testScreenReader() { - // 验证可访问性接口 - QAccessibleInterface* iface = QAccessible::queryAccessibleInterface(m_button); - - QCOMPARE(iface->role(), QAccessible::Role::PushButton); - QVERIFY(!iface->text(QAccessible::Name).isEmpty()); -} -```yaml - ---- - -## 六、性能测试 - -### 6.1 基准测试框架 - -```cpp -// tests/performance/benchmarks/benchmark_theme_load.cpp - -#include -#include -#include - -class BenchmarkThemeLoad : public QObject { - Q_OBJECT - -private slots: - void testThemeLoadPerformance(); - void testVariableLookupPerformance(); - void testQSSProcessingPerformance(); -}; +CFDesktop 的测试策略遵循经典测试金字塔:70% 单元测试 + 20% 集成测试 + 10% UI 测试。选择 GoogleTest 作为主框架(项目已在 CLAUDE.md 中明确),配合 Qt6::Test 处理信号/控件相关的测试。这个组合既能测纯逻辑(CPU 检测、主题引擎、动画管理),也能测 Qt 信号槽和 Widget 行为。 -void BenchmarkThemeLoad::testThemeLoadPerformance() { - ThemeEngine* engine = ThemeEngine::instance(); - QString themePath = "/path/to/default/theme"; +测试目录按 `test///_test.cpp` 组织,与源码结构镜像,便于定位。硬件相关测试通过 Mock 系统注入伪造的 /proc/cpuinfo、/proc/meminfo 等文件,避免测试依赖真实硬件。CI 使用 GitHub Actions,单元测试和集成测试在容器中运行,UI 测试通过 Xvfb 虚拟显示执行,性能测试单独 job 跑 Release 构建。 - QElapsedTimer timer; - timer.start(); +## 关键决策 - engine->loadTheme(themePath); - - qint64 elapsed = timer.elapsed(); - - // 加载时间应 < 100ms - QVERIFY(elapsed < 100); - - qDebug() << "Theme load time:" << elapsed << "ms"; -} - -void BenchmarkThemeLoad::testVariableLookupPerformance() { - ThemeEngine* engine = ThemeEngine::instance(); - engine->loadTheme("/path/to/default/theme"); - - const int iterations = 10000; - - QElapsedTimer timer; - timer.start(); - - for (int i = 0; i < iterations; ++i) { - engine->color("primary"); - engine->size("normal"); - engine->font("body"); - } - - qint64 elapsed = timer.elapsed(); - qreal avgTime = static_cast(elapsed) / iterations; - - // 平均查找时间应 < 0.01ms - QVERIFY(avgTime < 0.01); - - qDebug() << "Average variable lookup time:" << avgTime * 1000 << "μs"; -} -```text - -### 6.2 内存使用测试 - -```cpp -// tests/performance/test_memory_usage.cpp - -class TestMemoryUsage : public QObject { - Q_OBJECT - -private slots: - void testThemeEngineMemory(); - void testAnimationManagerMemory(); - void testMultipleWindowsMemory(); - -private: - qint64 getCurrentMemoryUsage(); -}; - -void TestMemoryUsage::testThemeEngineMemory() { - qint64 before = getCurrentMemoryUsage(); - - ThemeEngine* engine = ThemeEngine::instance(); - engine->loadTheme("/path/to/default/theme"); - - qint64 after = getCurrentMemoryUsage(); - qint64 used = after - before; - - // 内存使用应 < 50MB - QVERIFY(used < 50 * 1024 * 1024); - - qDebug() << "Theme engine memory usage:" << used / 1024 << "KB"; -} - -qint64 TestMemoryUsage::getCurrentMemoryUsage() { - // Linux: 读取 /proc/self/status - QFile file("/proc/self/status"); - if (file.open(QIODevice::ReadOnly)) { - QTextStream in(&file); - while (!in.atEnd()) { - QString line = in.readLine(); - if (line.startsWith("VmRSS:")) { - QString value = line.section(':', 1).section('k', 0, 0).trimmed(); - return value.toLongLong() * 1024; - } - } - } - return -1; -} -```bash - -### 6.3 性能基准 - -| 模块 | 指标 | 目标值 | 测试方法 | -|------|------|--------|----------| -| 主题加载 | 加载时间 | < 100ms | 计时测试 | -| 变量查询 | 平均时间 | < 0.01ms | 循环测试 | -| 动画启动 | 启动延迟 | < 16ms | 计时测试 | -| 动画帧率 | 稳定性 | 60fps | 帧率测试 | -| 输入响应 | 延迟 | < 16ms | 事件测试 | -| 内存使用 | 基础占用 | < 100MB | 内存测试 | -| 启动时间 | 冷启动 | < 2s | 计时测试 | - ---- - -## 七、CI/CD 配置 - -### 7.1 GitHub Actions 配置 - -**文件**: `.github/workflows/test.yml` - -```yaml -name: Test Suite - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - unit-tests: - runs-on: ubuntu-latest - container: ghcr.io/cfdesktop/builder-linux:latest - - steps: - - uses: actions/checkout@v3 - - - name: Configure - run: | - cmake -B build \ - -DCMAKE_BUILD_TYPE=Debug \ - -DBUILD_TESTING=ON \ - -DENABLE_COVERAGE=ON - - - name: Build - run: cmake --build build --parallel - - - name: Run Unit Tests - run: | - cd build - ctest --output-on-failure \ - --rerun-failed \ - -j$(nproc) - - - name: Generate Coverage - run: | - cd build - gcovr --xml-pretty --exclude-unreachable-branches \ - --print-summary -o coverage.xml - - - name: Upload Coverage - uses: codecov/codecov-action@v3 - with: - files: ./build/coverage.xml - - integration-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build - - - name: Run Integration Tests - run: | - cd build - ctest -R integration --output-on-failure - - ui-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build - - - name: Setup Virtual Display - run: | - Xvfb :99 -screen 0 1024x768x24 & - export DISPLAY=:99 - - - name: Run UI Tests - run: | - cd build - ctest -R ui --output-on-failure - - performance-tests: - runs-on: ubuntu-latest - needs: unit-tests - - steps: - - uses: actions/checkout@v3 - - - name: Build - run: | - cmake -B build -DBUILD_TESTING=ON - cmake --build build --release - - - name: Run Performance Tests - run: | - cd build - ctest -R performance --output-on-failure - - - name: Upload Results - uses: actions/upload-artifact@v3 - with: - name: performance-results - path: build/testing/performance/ -```bash - -### 7.2 代码覆盖率要求 - -| 模块 | 最低覆盖率 | 目标覆盖率 | -|------|-----------|-----------| -| HardwareProbe | 90% | 95% | -| ThemeEngine | 85% | 90% | -| AnimationManager | 85% | 90% | -| DPIManager | 85% | 90% | -| ConfigStore | 90% | 95% | -| Logger | 85% | 90% | -| InputManager | 80% | 85% | -| **整体** | **85%** | **90%** | - ---- - -## 八、测试数据管理 - -### 8.1 Mock 数据文件 - -**文件**: `tests/mock/proc/cpuinfo_imx6ull` - -```text -processor : 0 -model name : Freescale i.MX6 UltraLite 528 MHz -BogoMIPS : 264.00 -Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpd32 -CPU implementer : 0x41 -CPU architecture: 7 -CPU variant : 0x0 -CPU part : 0xc07 -CPU revision : 5 - -Hardware : Freescale i.MX6 UltraLite 528 MHz -Revision : 0000 -Serial : 0000000000000000 -```text - -**文件**: `tests/mock/proc/meminfo_512mb` - -```text -MemTotal: 524288 kB -MemFree: 262144 kB -MemAvailable: 393216 kB -Buffers: 16384 kB -Cached: 65536 kB -SwapCached: 0 kB -Active: 131072 kB -Inactive: 104857 kB -SwapTotal: 0 kB -SwapFree: 0 kB -```text - -### 8.2 测试夹具 - -```cpp -// tests/fixtures/test_theme.h - -namespace CFDesktop::Testing { - -/** - * @brief 测试主题生成器 - * - * 快速创建测试用主题。 - */ -class TestThemeBuilder { -public: - TestThemeBuilder& addColor(const QString& name, const QColor& color); - TestThemeBuilder& addSize(const QString& name, int size); - TestThemeBuilder& addFont(const QString& name, const QFont& font); - TestThemeBuilder& addStyle(const QString& name, const QString& qss); - - Theme build() const; - void save(const QString& path) const; - -private: - Theme m_theme; -}; - -/** - * @brief 便捷函数 - */ -inline Theme createDefaultTestTheme() { - return TestThemeBuilder() - .addColor("primary", QColor("#2196F3")) - .addColor("background", QColor("#FFFFFF")) - .addSize("normal", 8) - .addFont("body", QFont("Arial", 10)) - .build(); -} - -} // namespace CFDesktop::Testing -```bash - ---- - -## 九、详细任务清单 - -### 9.1 基础设施 (Week 1) - -- [ ] 配置 CMake 测试框架 -- [ ] 创建测试目录结构 -- [ ] 实现 Mock 系统 -- [ ] 创建测试应用框架 -- [ ] 配置 CI 基础流水线 - -### 9.2 单元测试 (Week 2-4) - -- [ ] HardwareProbe 测试套件 -- [ ] ThemeEngine 测试套件 -- [ ] AnimationManager 测试套件 -- [ ] DPIManager 测试套件 -- [ ] ConfigStore 测试套件 -- [ ] Logger 测试套件 -- [ ] InputManager 测试套件 - -### 9.3 集成测试 (Week 5) - -- [ ] Base 库集成测试 -- [ ] SDK 集成测试 -- [ ] Shell 集成测试 - -### 9.4 UI 测试 (Week 6) - -- [ ] Launcher UI 测试 -- [ ] WindowManager UI 测试 -- [ ] 应用切换测试 - -### 9.5 性能测试 (Week 7) - -- [ ] 基准测试框架 -- [ ] 启动时间测试 -- [ ] 内存使用测试 -- [ ] 动画性能测试 -- [ ] 建立性能基准 - ---- - -## 十、验收标准 - -### 10.1 测试覆盖率 -- [ ] 整体代码覆盖率 > 85% -- [ ] 核心模块覆盖率 > 90% -- [ ] 关键路径覆盖率 100% - -### 10.2 CI/CD -- [ ] 所有测试自动运行 -- [ ] 测试失败阻止合并 -- [ ] 覆盖率报告自动生成 - -### 10.3 性能 -- [ ] 所有性能测试达标 -- [ ] 无内存泄漏 -- [ ] 无明显性能退化 - ---- - -## 十一、测试最佳实践 - -### 11.1 命名约定 - -- 测试文件: `test_.cpp` -- 测试类: `Test` -- 测试用例: `test_` - -### 11.2 断言选择 - -| 场景 | 使用 | -|------|------| -| 简单比较 | `QVERIFY2`, `QCOMPARE` | -| 性能测试 | `QBenchmark` | -| 异常测试 | `QEXPECT_FAIL` | -| 跳过测试 | `QSKIP` | - -### 11.3 测试隔离 - -```cpp -void TestExample::init() { - // 每个测试用例前:重置状态 - m_testObject = new TestClass(); -} - -void TestExample::cleanup() { - // 每个测试用例后:清理资源 - delete m_testObject; -} -```bash - ---- - -## 十二、常见问题解决 - -| 问题 | 解决方案 | -|------|----------| -| Qt 测试找不到组件 | 设置 `QT_QPA_PLATFORM=offscreen` | -| CI 中 UI 测试失败 | 使用 `Xvfb` 虚拟显示 | -| Mock 数据路径错误 | 使用 `QFINDTESTDATA` | -| 测试相互影响 | 确保正确使用 `init()`/`cleanup()` | - ---- +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| GoogleTest + Qt6::Test 双框架 | GoogleTest 适合纯逻辑单元测试,Qt::Test 提供 QSignalSpy 等 Qt 专项支持 | 纯 GoogleTest(Qt 信号测试麻烦)或纯 QtTest(缺少参数化等高级特性) | +| Mock 文件注入(伪造 /proc、/sys) | 硬件探测逻辑依赖内核文件系统,Mock 让测试在任何机器上可复现 | 每次测试都要求特定硬件环境 | +| 测试金字塔 70/20/10 | 单元测试快速稳定覆盖大部分逻辑,集成/UI 测试聚焦关键路径 | 均衡分配或只做单元测试 | +| CI 分阶段 (unit -> integration -> UI -> performance) | 单元测试快速失败尽早反馈,UI/性能测试依赖前序通过再执行 | 所有测试一次性全跑 | -## 十三、下一步行动 +## 当前状态 -测试体系应与其他阶段并行开发: -- Phase 0 完成 CI 基础设施 -- Phase 1-2 同步编写单元测试 -- Phase 4-5 同步编写集成和 UI 测试 +部分实现(约 55%)。base 层核心模块已有单元测试覆盖,CI 已配置 GitHub Actions。参见 [status/current.md](../status/current.md) Phase 8 部分。 diff --git a/document/design_stage/README.md b/document/design_stage/README.md deleted file mode 100644 index 5850f0e41..000000000 --- a/document/design_stage/README.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: CFDesktop 前期阶段设计文档总览 -description: 本目录包含 CFDesktop 项目前期阶段的详细设计文档。 ---- - -# CFDesktop 前期阶段设计文档总览 - -## 文档目录 - -本目录包含 CFDesktop 项目前期阶段的详细设计文档。 - -### 文档列表 - -| 文档 | 阶段 | 内容概要 | -|------|------|----------| -| [system_architecture_overview.md](system_architecture_overview.md) | 总览 | 系统架构总览 — 模块层次、核心接口、平台抽象层、显示后端、UI 分层、初始化流程 | -| [multi_display_backend_architecture.md](multi_display_backend_architecture.md) | 架构 | 多显示后端架构 — 运行时后端选择、组件复用矩阵、各后端实现指导 | -| [00_phase0_project_skeleton.md](00_phase0_project_skeleton.md) | Phase 0 | 工程骨架搭建 - CMake 构建系统、目录结构、CI/CD 配置 | -| [01_phase1_hardware_probe.md](01_phase1_hardware_probe.md) | Phase 1 | 硬件探针与能力分级 - CPU/GPU/内存检测、HWTier 档位判定 | -| [02_phase2_base_library.md](02_phase2_base_library.md) | Phase 2 | Base 库核心 - 主题引擎、动画管理、DPI 适配、配置中心、日志系统 | -| [03_phase3_input_layer.md](03_phase3_input_layer.md) | Phase 3 | 输入抽象层 - 触摸/按键/旋钮处理、手势识别、焦点导航 | -| [04_phase6_simulator.md](04_phase6_simulator.md) | Phase 6 | 多平台模拟器 - PC 端模拟器、设备外壳、触摸可视化、参数注入 | -| [05_phase8_testing.md](05_phase8_testing.md) | Phase 8 | 测试体系 - 单元测试、集成测试、UI 测试、CI/CD | - ---- - -## 快速导航 - -### 按角色查找 - -#### 开发者 -- **新手上路**: 从 [Phase 0](00_phase0_project_skeleton.md) 开始,了解项目结构 -- **基础设施**: [Phase 1](01_phase1_hardware_probe.md) + [Phase 2](02_phase2_base_library.md) -- **UI 开发**: [Phase 3](03_phase3_input_layer.md) -- **调试工具**: [Phase 6](04_phase6_simulator.md) - -#### 测试工程师 -- **测试框架**: [Phase 8](05_phase8_testing.md) -- **单元测试**: 各 Phase 文档中的"单元测试"章节 - -#### 项目经理 -- **时间线**: 查看 BLUEPRINT.md 中的里程碑时间线 -- **任务分解**: 各 Phase 文档中的"详细任务清单"章节 - -### 按模块查找 - -| 模块 | 文档 | -|------|------| -| 系统架构 | [系统架构总览](system_architecture_overview.md) | -| 多显示后端 | [多显示后端架构](multi_display_backend_architecture.md) | -| 目录结构 | [Phase 0 - 二、目录结构设计](00_phase0_project_skeleton.md#二目录结构设计) | -| 构建系统 | [Phase 0 - 三、CMake 构建系统设计](00_phase0_project_skeleton.md#三cmake-构建系统设计) | -| 硬件检测 | [Phase 1 - 五、检测逻辑详细设计](01_phase1_hardware_probe.md#五检测逻辑详细设计) | -| 主题引擎 | [Phase 2 - 三、主题引擎](02_phase2_base_library.md#三主题引擎-themeengine) | -| 动画管理 | [Phase 2 - 四、动画管理器](02_phase2_base_library.md#四动画管理器-animationmanager) | -| DPI 适配 | [Phase 2 - 五、DPI 管理器](02_phase2_base_library.md#五dpi-管理器-dpimanager) | -| 配置系统 | [Phase 2 - 六、配置中心](02_phase2_base_library.md#六配置中心-configstore) | -| 日志系统 | [Phase 2 - 七、日志系统](02_phase2_base_library.md#七日志系统-logger) | -| 输入处理 | [Phase 3](03_phase3_input_layer.md) | -| 模拟器 | [Phase 6](04_phase6_simulator.md) | -| 测试 | [Phase 8](05_phase8_testing.md) | - ---- - -## 使用建议 - -### 阅读顺序 -1. 先阅读 [BLUEPRINT.md](../../BLUEPRINT.md) 了解整体规划 -2. 阅读 [Phase 0](00_phase0_project_skeleton.md) 了解工程结构 -3. 根据开发任务阅读对应 Phase 文档 - -### 开发流程 -1. **环境搭建**: 参考 Phase 0 创建项目骨架 -2. **基础模块**: 按 Phase 1 → Phase 2 → Phase 3 顺序开发 -3. **Shell UI**: 参考 BLUEPRINT.md Phase 4 -4. **SDK 层**: 参考 BLUEPRINT.md Phase 5 -5. **模拟器**: 参考 Phase 6 -6. **测试**: 参考 Phase 8,贯穿全程 - -### 文档更新 -- 设计文档会随开发进展更新 -- 变更记录请查看各文档的版本信息 -- 有问题请及时反馈 - ---- - -## 项目里程碑 - -| 里程碑 | 时间 | 交付物 | -|--------|------|--------| -| M0 | Week 2 | 工程骨架 + CI 跑通 | -| M1 | Week 5 | 硬件探针 + 三档能力分级 | -| M2 | Week 9 | Base 库 + 主题引擎 + 输入抽象 | -| M3 | Week 15 | Shell UI 主体可用 | -| M4 | Week 18 | SDK 导出 + 示例应用 | -| M5 | Week 21 | 模拟器可用 | -| M6 | Week 23 | 应用商店基础 + 完整 CI/CD | - ---- - -## 联系方式 - -如有问题或建议,请通过以下方式联系: -- GitHub Issues: [项目 Issues 页面] -- 技术讨论: [项目 Discussions 页面] - ---- - -*最后更新: 2026-03-30* diff --git a/document/design_stage/index.md b/document/design_stage/index.md index 81bf25b77..594c51238 100644 --- a/document/design_stage/index.md +++ b/document/design_stage/index.md @@ -1,11 +1,52 @@ --- -title: 设计阶段文档 -description: 本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完 +title: CFDesktop 前期阶段设计文档总览 +description: 本目录包含 CFDesktop 项目前期阶段的详细设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。 --- -# 设计阶段文档 +# CFDesktop 前期阶段设计文档总览 -本目录包含 CFDesktop 项目各阶段的设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。内容包括系统整体架构设计、多显示后端方案、硬件探测层(Hardware Probe)设计、基础库(Base Library)架构以及输入抽象层(Input Layer)设计等核心模块的技术方案。 +本目录包含 CFDesktop 项目前期阶段的详细设计文档,覆盖 Phase 0 至 Phase 8 的完整规划。内容包括系统整体架构设计、多显示后端方案、硬件探测层设计、基础库架构以及输入抽象层设计等核心模块的技术方案。 + +## 文档列表 + +| 文档 | 阶段 | 内容概要 | +|------|------|----------| +| [system_architecture_overview.md](system_architecture_overview.md) | 总览 | 系统架构总览 — 模块层次、核心接口、平台抽象层、显示后端、UI 分层、初始化流程 | +| [multi_display_backend_architecture.md](multi_display_backend_architecture.md) | 架构 | 多显示后端架构 — 运行时后端选择、组件复用矩阵、各后端实现指导 | +| [00_phase0_project_skeleton.md](00_phase0_project_skeleton.md) | Phase 0 | 工程骨架搭建 - CMake 构建系统、目录结构、CI/CD 配置 | +| [01_phase1_hardware_probe.md](01_phase1_hardware_probe.md) | Phase 1 | 硬件探针与能力分级 - CPU/GPU/内存检测、HWTier 档位判定 | +| [02_phase2_base_library.md](02_phase2_base_library.md) | Phase 2 | Base 库核心 - 主题引擎、动画管理、DPI 适配、配置中心、日志系统 | +| [03_phase3_input_layer.md](03_phase3_input_layer.md) | Phase 3 | 输入抽象层 - 触摸/按键/旋钮处理、手势识别、焦点导航 | +| [04_phase6_simulator.md](04_phase6_simulator.md) | Phase 6 | 多平台模拟器 - PC 端模拟器、设备外壳、触摸可视化、参数注入 | +| [05_phase8_testing.md](05_phase8_testing.md) | Phase 8 | 测试体系 - 单元测试、集成测试、UI 测试、CI/CD | + +--- + +## 按模块查找 + +| 模块 | 文档 | +|------|------| +| 系统架构 | [系统架构总览](system_architecture_overview.md) | +| 多显示后端 | [多显示后端架构](multi_display_backend_architecture.md) | +| 目录结构 | [Phase 0](00_phase0_project_skeleton.md) | +| 构建系统 | [Phase 0](00_phase0_project_skeleton.md) | +| 硬件检测 | [Phase 1](01_phase1_hardware_probe.md) | +| 主题引擎 | [Phase 2](02_phase2_base_library.md) | +| 动画管理 | [Phase 2](02_phase2_base_library.md) | +| DPI 适配 | [Phase 2](02_phase2_base_library.md) | +| 配置系统 | [Phase 2](02_phase2_base_library.md) | +| 日志系统 | [Phase 2](02_phase2_base_library.md) | +| 输入处理 | [Phase 3](03_phase3_input_layer.md) | +| 模拟器 | [Phase 6](04_phase6_simulator.md) | +| 测试 | [Phase 8](05_phase8_testing.md) | + +--- + +## 阅读顺序 + +1. 先阅读系统架构总览了解整体设计 +2. 从 Phase 0 开始了解工程结构 +3. 根据开发任务阅读对应 Phase 文档 --- diff --git a/document/design_stage/multi_display_backend_architecture.md b/document/design_stage/multi_display_backend_architecture.md index 54974034f..f1b334b80 100644 --- a/document/design_stage/multi_display_backend_architecture.md +++ b/document/design_stage/multi_display_backend_architecture.md @@ -1,241 +1,37 @@ --- title: CFDesktop 多显示后端架构设计 -description: "最后更新: 2026-03-29,在 中增加:" +description: "多后端运行时选择、三种显示角色模型、组件复用矩阵的设计意图" --- -# CFDesktop 多显示后端架构设计 +# CFDesktop 多显示后端架构设计 -- 设计意图 -> **状态**: 设计中 -> **版本**: 0.1.0 -> **最后更新**: 2026-03-29 +## 为什么选择这种方案 ---- - -## 一、设计目标 - -CFDesktop 需要在以下场景中运行: - -| 场景 | 角色定位 | 运行环境 | -|------|---------|---------| -| Windows 伪桌面 | 客户端应用 | Windows 10/11,覆盖在 Windows 桌面上 | -| Linux 有桌面环境 | 客户端应用 | 有 Gnome/KDE 等完整桌面环境 | -| Linux 无桌面 (X11) | X11 窗口管理器 | 仅有 libx11/libxcb,无窗口管理器 | -| Linux 无桌面 (Wayland) | Wayland 合成器 | 仅有 libwayland,无合成器 | -| Linux 嵌入式 | 直接渲染 | 仅 libdrm/libgbm 或 linuxfb | - -核心设计原则:**同一套 Shell/Panel/WindowManager 代码在所有场景中复用,通过抽象层屏蔽平台差异。** - ---- - -## 二、系统角色模型 - -```text -┌─────────────────────────────────────────────────────┐ -│ CFDesktop Shell │ -│ (ShellLayer, PanelManager, WindowManager, IWindow) │ -├───────────────────────────────────────────────────────┤ -│ IDisplayServerBackend │ -│ ┌─────────┬──────────┬──────────┬────────────────┐ │ -│ │ Client │Compositor│Compositor│ DirectRender │ │ -│ │ Mode │ X11 WM │ Wayland │ EGLFS/FB │ │ -│ └────┬────┴────┬─────┴────┬─────┴───────┬────────┘ │ -│ │ │ │ │ │ -│ ┌────▼────┐┌───▼────┐┌───▼────┐┌───────▼──────┐ │ -│ │QWidget ││XCB WM ││QtWayland││Qt linuxfb/ │ │ -│ │Windows ││EWMH ││Compositor││EGLFS plugin │ │ -│ └─────────┘└────────┘└────────┘└──────────────┘ │ -├───────────────────────────────────────────────────────┤ -│ RenderBackend │ -│ (RHI abstraction, swapBuffers, capabilities) │ -├───────────────────────────────────────────────────────┤ -│ 硬件层 (GPU / DRM / FB / Win32) │ -└───────────────────────────────────────────────────────┘ -```bash - -### DisplayServerRole 枚举 - -| 值 | 含义 | 典型场景 | -|----|------|---------| -| `Client` | 在已有桌面环境中作为应用运行 | Windows, Linux with DE | -| `Compositor` | 自己就是显示服务器/合成器 | 无桌面 Linux (X11/Wayland) | -| `DirectRender` | 直接渲染到 framebuffer | 嵌入式设备 (EGLFS/linuxfb) | - ---- - -## 三、新增接口定义 - -### 3.1 IDisplayServerBackend (新增) - -**文件**: `desktop/ui/components/IDisplayServerBackend.h` - -```cpp -class IDisplayServerBackend : public QObject { - Q_OBJECT -public: - virtual DisplayServerRole role() const = 0; - virtual DisplayServerCapabilities capabilities() const = 0; - virtual bool initialize(int argc, char** argv) = 0; - virtual void shutdown() = 0; - virtual int runEventLoop() = 0; - virtual WeakPtr windowBackend() = 0; - virtual QList outputs() const = 0; - -signals: - void outputChanged(); - void externalWindowAppeared(WeakPtr window); - void externalWindowDisappeared(WeakPtr window); -}; -```text - -**职责**: -- 决定 CFDesktop 的运行角色(客户端 / 合成器 / 直接渲染) -- 管理显示输出的生命周期 -- 在合成器模式下,桥接外部窗口事件到 WindowManager - -### 3.2 RenderBackend (新增) - -**文件**: `desktop/ui/render/render_backend.h` - -```cpp -class RenderBackend { -public: - virtual ~RenderBackend() = default; - virtual bool initialize() = 0; - virtual void shutdown() = 0; - virtual BackendCapabilities capabilities() const = 0; - virtual QSize screenSize() const = 0; - virtual void* nativeHandle() const = 0; // EGLDisplay, HWND 等 -}; -```text - -**职责**: -- 抽象底层渲染硬件初始化 -- 提供渲染能力查询 -- 不负责具体绘制(由 Qt RHI 或 QPainter 负责) - -### 3.3 BackendCapabilities (新增) - -**文件**: `desktop/ui/render/backend_capabilities.h` - -```cpp -struct BackendCapabilities { - bool supportsMultiWindow = true; - bool supportsTransparency = true; - bool hasHardwareAcceleration = true; - bool supportsVSync = true; - bool supportsScreenshot = true; - int maxTextureSize = 4096; -}; -```yaml - ---- - -## 四、现有接口修改 - -### 4.1 IWindowBackend — 增加 capabilities() +CFDesktop 需要在五种运行场景中工作:Windows 客户端、Linux 有桌面环境客户端、Linux 无桌面 X11 窗口管理器、Linux 无桌面 Wayland 合成器、嵌入式直接渲染。这些场景对显示后端的需求截然不同 -- 有的只需跟踪外部窗口,有的需要自己充当合成器,有的甚至连窗口系统都没有。 -在 `IWindowBackend` 中增加: -```cpp -virtual BackendCapabilities capabilities() const = 0; -```text +核心设计原则是:**同一套 Shell / Panel / WindowManager 代码在所有场景中复用,通过 `IDisplayServerBackend` 抽象层屏蔽平台差异。** 系统定义了三种运行角色(`Client` / `Compositor` / `DirectRender`),每个后端实现一种角色。运行时通过 `DetectDisplayServerMode()` 自动检测环境(环境变量 -> Wayland/X11 socket -> DRM 设备 -> framebuffer),选择合适的后端实例化。上层代码通过 `capabilities()` 查询能力,对不具备的功能优雅降级。 -允许 WindowManager 和 Shell 在运行时查询后端能力,做出适配决策。 +## 关键决策 -### 4.2 qt_backend.h — 扩展枚举和检测 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三种运行角色(Client/Compositor/DirectRender) | 覆盖所有已知场景,每种角色的能力边界清晰 | 只区分"有/无桌面环境"(无法处理嵌入式场景) | +| 运行时自动检测后端 | 用户无需配置即可适配当前环境,降低使用门槛 | 编译时硬编码后端(每个平台单独编译,维护成本翻倍) | +| `IDisplayServerBackend` 作为顶层抽象 | 角色检测、能力查询、生命周期管理集中在一个接口,上层只需对接这一层 | 每个平台各自暴露不同接口(上层需要大量条件分支) | +| `BackendCapabilities` 能力查询 | 允许上层在运行时根据实际能力做降级决策(如嵌入式不支持多窗口) | 假设所有能力都可用(在功能受限的嵌入式设备上崩溃) | +| `IShellLayer` 接口解耦 QWidget | Wayland 合成器不继承 QWidget,而是实现纯接口 `IShellLayer` | 强制所有后端都基于 QWidget(Wayland 原生合成器无法实现) | -```cpp -enum class LinuxQtBackend { - X11, // X11 窗口系统 - Wayland, // Wayland 合成器 - EGLFS, // EGL + OpenGL ES (无窗口系统) - LinuxFB, // Linux 帧缓冲 (纯软件渲染) - Unknown -}; +## 平台角色模型 -enum class DisplayServerMode { - Client, // 有现成的窗口系统可用 - NeedCompositor, // 需要自己做合成器 - DirectRender // 直接渲染到硬件 -}; +| 场景 | 角色 | 典型技术 | +|------|------|---------| +| Windows 伪桌面 | Client | QWidget + Win32 API + SetWinEventHook | +| Linux 有桌面环境 | Client | QWidget + X11/Wayland 客户端 | +| Linux 无桌面 (X11) | Compositor | XCB SubstructureRedirect + EWMH + XComposite | +| Linux 无桌面 (Wayland) | Compositor | QtWaylandCompositor + xdg-shell + DRM/KMS | +| Linux 嵌入式 | DirectRender | Qt EGLFS/linuxfb + libdrm + evdev | -DisplayServerMode DetectDisplayServerMode(); -```text - -`DetectDisplayServerMode()` 检测逻辑: -1. 检查环境变量 `CFDESKTOP_DISPLAY_SERVER` (强制覆盖) -2. 检查 `WAYLAND_DISPLAY` 或 `DISPLAY` (有显示服务器 → Client) -3. 检查 `/dev/dri/card*` (可做 EGLFS → DirectRender) -4. 检查 `/dev/fb0` (可做 linuxfb → DirectRender) -5. 默认: Client - -### 4.3 ShellLayer — 解耦 QWidget - -新增纯接口 `IShellLayer`: -```cpp -class IShellLayer { -public: - virtual ~IShellLayer() = default; - virtual void setStrategy(std::unique_ptr strategy) = 0; - virtual QRect geometry() const = 0; -}; -```text - -`ShellLayer` 同时继承 `IShellLayer` 和 `QWidget`: -```cpp -class ShellLayer : public QWidget, public IShellLayer { - // QWidget 实现 — Client 模式 -}; - -// Wayland 合成器可以实现 IShellLayer 而不继承 QWidget -```bash - ---- - -## 五、各后端实现指导 - -### 5.1 Windows 伪桌面 - -- **角色**: `DisplayServerRole::Client` -- **RenderBackend**: WindowsBackend (封装 Win32 + Qt RHI) -- **IWindowBackend**: 每个窗口 = QWidget 子控件 -- **特殊处理**: 无边框全屏 + WS_EX_TOOLWINDOW 避免任务栏 -- **现有代码复用**: `windows_display_size_policy.cpp` - -### 5.2 X11 客户端 - -- **角色**: `DisplayServerRole::Client` -- **RenderBackend**: 默认 Qt RHI -- **IWindowBackend**: QWidget 直接映射为 X11 Window -- **现有代码复用**: `linux_wsl_display_size_policy.cpp` 的 X11 路径 - -### 5.3 X11 窗口管理器 (无桌面) - -- **角色**: `DisplayServerRole::Compositor` -- **关键技术**: XCB SubstructureRedirect + EWMH + XComposite -- **IWindowBackend**: XCB Window → `IWindow` 适配器 -- **合成**: XComposite 重定向到 offscreen pixmap, OpenGL 合成 -- **输入**: XCB 事件循环转发 - -### 5.4 Wayland 合成器 (无桌面) - -- **角色**: `DisplayServerRole::Compositor` -- **关键技术**: QtWaylandCompositor C++ API + xdg-shell -- **IWindowBackend**: QWaylandSurface → `IWindow` 适配器 -- **合成**: Qt RHI (OpenGL/Vulkan) 直接合成 -- **输出**: DRM/KMS via QWaylandOutput -- **依赖**: Qt6::WaylandCompositor, libwayland-server, libdrm - -### 5.5 EGLFS / linuxfb - -- **角色**: `DisplayServerRole::DirectRender` -- **关键技术**: Qt EGLFS/linuxfb 平台插件 -- **IWindowBackend**: 全部"窗口" = QWidget 子控件 -- **输入**: evdev (udev) 直接读取 -- **限制**: 无多窗口、无外部应用管理 - ---- - -## 六、组件复用矩阵 +## 组件复用矩阵 | 组件 | Client | X11 WM | Wayland | DirectRender | |------|--------|--------|---------|-------------| @@ -246,55 +42,6 @@ class ShellLayer : public QWidget, public IShellLayer { | `ShellLayer` | QWidget 实现 | QWidget 实现 | IShellLayer 实现 | QWidget 实现 | | `IShellLayerStrategy` | 直接复用 | 直接复用 | 需新策略 | 直接复用 | ---- - -## 七、构建系统设计 - -```cmake -# 条件编译选项 -option(CFDESKTOP_ENABLE_WAYLAND_COMPOSITOR "Enable Wayland compositor backend" ON) -option(CFDESKTOP_ENABLE_X11_WM "Enable X11 window manager backend" ON) -option(CFDESKTOP_ENABLE_LINUXFB "Enable linuxfb backend" ON) - -# 自动检测依赖 -if(CFDESKTOP_ENABLE_WAYLAND_COMPOSITOR) - find_package(Qt6 OPTIONAL_COMPONENTS WaylandCompositor) -endif() - -if(CFDESKTOP_ENABLE_X11_WM) - find_package(X11) - find_package(PkgConfig) - pkg_check_modules(XCB OPTIONAL xcb xcb-composite xcb-ewmh) -endif() -```yaml - ---- - -## 八、运行时后端选择流程 +## 当前状态 -```text -CFDesktop 启动 - │ - ▼ -DetectDisplayServerMode() - │ - ├── 环境变量 CFDESKTOP_DISPLAY_SERVER? - │ └── 强制使用指定后端 - │ - ├── WAYLAND_DISPLAY 存在? - │ └── Client 模式 (Wayland 客户端) - │ - ├── DISPLAY 存在? - │ ├── 有其他 WM 在运行? - │ │ └── Client 模式 (X11 客户端) - │ └── 无 WM? - │ └── Compositor 模式 (X11 WM) - │ - ├── /dev/dri/card* 存在? - │ └── DirectRender 模式 (EGLFS) - │ - ├── /dev/fb0 存在? - │ └── DirectRender 模式 (linuxfb) - │ - └── 默认: Client 模式 -```text +部分实现。Windows Client 和 WSL X11 Client 后端已完成;X11 WM、Wayland 合成器、EGLFS/linuxfb 后端规划中。详细接口规格参见 HandBook。 diff --git a/document/design_stage/system_architecture_overview.md b/document/design_stage/system_architecture_overview.md index fe8e880f8..1f7d64f7f 100644 --- a/document/design_stage/system_architecture_overview.md +++ b/document/design_stage/system_architecture_overview.md @@ -1,618 +1,41 @@ --- title: CFDesktop 系统架构总览 -description: "版本: 0.13.1,最后更新: 2026-03-30" +description: "三层单向依赖架构、平台抽象层、DAG 初始化链的设计意图" --- -# CFDesktop 系统架构总览 +# CFDesktop 系统架构总览 -- 设计意图 -> **状态**: 已实现 -> **版本**: 0.13.1 -> **最后更新**: 2026-03-30 +## 为什么选择这种方案 ---- - -## 一、项目概述 - -**CFDesktop** 是一个基于 Qt 6 / C++23 的 Material Design 3 桌面环境框架,面向嵌入式设备设计,同时支持在 Windows 和 Linux/WSL 上以 Client 模式运行。 - -| 属性 | 值 | -|------|-----| -| 定位 | 便携式桌面环境框架 | -| 版本 | 0.13.1 | -| 语言 | C++23 | -| UI 框架 | Qt 6.8.3 | -| 设计系统 | Material Design 3 | -| 构建系统 | CMake 3.16+ | -| 目标平台 | Windows 10/11, Linux/WSL, 嵌入式 ARM | - -### 设计目标 - -1. **跨平台统一**:同一套 Shell / Panel / WindowManager 代码在所有平台复用 -2. **Material Design 3**:完整的 MD3 设计系统实现(颜色、排版、形状、动效) -3. **嵌入式适配**:动态检测硬件能力,优雅降级到低性能模式 -4. **可扩展性**:通过 Factory / Strategy 模式轻松添加新平台后端 - ---- - -## 二、模块层次与依赖 - -### 2.1 三层模块架构 - -```text -┌─────────────────────────────────────────────────────────┐ -│ desktop 模块 │ -│ 桌面环境实现 — Shell, Panel, 后端 │ -├─────────────────────────────────────────────────────────┤ -│ ui 模块 │ -│ UI 框架 — Material Design 3 组件与动画 │ -├─────────────────────────────────────────────────────────┤ -│ base 模块 │ -│ 基础库 — 工具、系统检测、工厂模式基础设施 │ -├─────────────────────────────────────────────────────────┤ -│ Qt 6 / OS API │ -└─────────────────────────────────────────────────────────┘ -```bash - -**依赖规则**:`desktop` → `ui` → `base`,严格单向依赖。 - -### 2.2 模块职责一览 - -| 模块 | 子模块 | 构建目标 | 职责 | -|------|--------|----------|------| -| **base** | `include/` | `cfbase_headers` | 头文件工具库(Factory、Singleton、WeakPtr、宏) | -| | `system/cpu/` | `cfbase_cpu` | CPU 检测与性能分级 | -| | `system/memory/` | `cfbase_memory` | 内存检测 | -| | `system/gpu/` | `cfbase_gpu` | GPU 检测 | -| | `system/network/` | `cfbase_network` | 网络状态检测 | -| | `device/console/` | `cfbase_console` | 控制台工具 | -| | — | **`cfbase`** | 统一聚合静态库 | -| **ui** | `base/` | `cf_ui_base` | 数学、颜色、几何、easing | -| | `core/` | `cf_ui_core` | 主题引擎、设计 Token | -| | `components/` | `cf_ui_components` | 动画框架 | -| | `widget/material/` | `cf_ui_components_material` | Material Design 3 控件 | -| | `widget/` | `cf_ui_widget` | Widget 适配层 | -| | `models/` | — | 数据模型 | -| | — | **`cfui`** | 统一聚合静态库 | -| **desktop** | `main/` | `CFDesktopMain` | 初始化链、启动入口 | -| | `ui/components/` | `cf_desktop_components` | 核心接口(IWindow, IWindowBackend, IDisplayServerBackend) | -| | `ui/platform/` | `cf_desktop_ui_platform` | 平台抽象层(Windows / WSL) | -| | `ui/render/` | `cf_desktop_render` | 渲染后端抽象 | -| | `ui/widget/` | — | 桌面特有 Widget | -| | `base/` | — | 配置、日志、文件操作 | -| | — | **`CFDesktop_shared`** | 统一聚合共享库(.dll/.so) | -| | — | **`CFDesktop`** | 薄壳可执行文件 | - -### 2.3 最终构建产物 - -```text -CFDesktop.exe / CFDesktop ← 薄壳 main.cpp - └── CFDesktop_shared.dll/.so ← 统一共享库(聚合所有静态库) - ├── CFDesktopMain ← 初始化与启动 - ├── CFDesktopUi ← 桌面 UI 聚合 - │ ├── cf_desktop_ui_platform ← 平台抽象层 - │ ├── cf_desktop_components ← 核心接口 - │ └── cf_desktop_render ← 渲染抽象 - ├── cfui ← UI 框架聚合 - │ ├── cf_ui_base - │ ├── cf_ui_core - │ ├── cf_ui_components - │ └── cf_ui_widget - ├── cfbase ← 基础库聚合 - │ ├── cfbase_cpu - │ ├── cfbase_memory - │ ├── cfbase_gpu - │ └── cfbase_console - ├── cflogger - ├── cffilesystem - ├── cfconfig - └── cfasciiart -```yaml - ---- - -## 三、核心接口体系 - -### 3.1 接口层次关系 - -```text -┌─────────────────────────────────────────────────────┐ -│ IDisplayServerBackend │ -│ (顶层抽象:角色、能力、生命周期、事件循环) │ -│ │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ IWindowBackend │ │ -│ │ (窗口创建/销毁/跟踪,发出 window_came/gone) │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────┐ │ │ -│ │ │ IWindow │ │ │ -│ │ │ (单个窗口:ID、标题、几何、关闭、置顶) │ │ │ -│ │ └──────────────────────────────────────────┘ │ │ -│ └────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -```text - -### 3.2 IDisplayServerBackend - -**文件**: `desktop/ui/components/IDisplayServerBackend.h` - -顶层显示后端抽象,定义 CFDesktop 在显示栈中的角色: - -```cpp -enum class DisplayServerRole { - Client, // 在已有桌面环境中运行 - Compositor, // CFDesktop 就是显示服务器 - DirectRender // 直接渲染到 framebuffer -}; - -struct DisplayServerCapabilities { - DisplayServerRole role; - bool canManageExternalWindows; // 能否管理外部应用窗口 - bool needsOwnCompositor; // 是否需要自己的合成场景 - bool supportsWaylandProtocol; - bool supportsX11Protocol; -}; -```bash - -**核心方法**: - -| 方法 | 说明 | -|------|------| -| `role()` | 返回运行角色 | -| `capabilities()` | 返回能力描述 | -| `initialize(argc, argv)` | 初始化后端 | -| `shutdown()` | 关闭后端 | -| `runEventLoop()` | 运行事件循环(通常调用 `QApplication::exec()`) | -| `windowBackend()` | 返回窗口后端弱引用 | -| `outputs()` | 返回屏幕几何列表 | - -**信号**: - -| 信号 | 触发时机 | -|------|---------| -| `outputChanged()` | 屏幕增减或分辨率变化 | -| `externalWindowAppeared()` | 外部应用窗口出现 | -| `externalWindowDisappeared()` | 外部应用窗口消失 | - -### 3.3 IWindowBackend - -**文件**: `desktop/ui/components/IWindowBackend.h` - -窗口创建与跟踪接口: - -| 方法 | 说明 | -|------|------| -| `createWindow(appId)` | 创建内部窗口 | -| `destroyWindow(window)` | 销毁窗口 | -| `windows()` | 返回所有跟踪中的窗口 | -| `capabilities()` | 返回渲染能力 | -| `make_weak()` | 返回后端弱引用 | - -**信号**: `window_came(WeakPtr)`, `window_gone(WeakPtr)` - -### 3.4 IWindow - -**文件**: `desktop/ui/components/IWindow.h` - -平台无关的窗口抽象: - -| 方法 | 说明 | -|------|------| -| `windowID()` | 唯一标识符(`win_id_t` = `QString`) | -| `title()` | 窗口标题 | -| `geometry()` | 窗口几何(设备像素) | -| `set_geometry(r)` | 移动/缩放窗口 | -| `requestClose()` | 请求关闭 | -| `raise()` | 置顶 | - -**信号**: `closeRequested()`, `titleChanged()`, `geometryChanged()` - -### 3.5 信号流转 - -```text -外部窗口出现(OS 层事件) - │ - ▼ -IWindowBackend::registerWindow() ← 平台后端检测到新窗口 - │ - ├─ 创建 IWindow 实例 - ├─ emit window_came(weak) - │ - ▼ -IDisplayServerBackend ← 转发信号 - │ (connect IWindowBackend::window_came → externalWindowAppeared) - │ - ▼ -CFDesktopEntity::run_init() ← 日志记录 + Shell 分发 - │ (connect IWindowBackend::window_came → slot) - │ - ▼ -WindowManager / ShellLayer ← UI 响应 -```yaml - ---- - -## 四、平台抽象层 - -### 4.1 设计模式 - -平台抽象层使用两种互补的设计模式: - -**Factory 模式** — 创建平台特定对象: - -```text -┌─────────────────────────────────────────────────┐ -│ RegisteredFactory │ -│ creator_: std::function │ -│ destroyer_: std::function │ -│ │ -│ make_unique() → unique_ptr│ -│ make_shared() → shared_ptr │ -├─────────────────────────────────────────────────┤ -│ StaticRegisteredFactory │ -│ (单例 + RegisteredFactory) │ -├─────────────────────────────────────────────────┤ -│ DisplayServerBackendFactory │ -│ : StaticRegisteredFactory│ -└─────────────────────────────────────────────────┘ -```text - -**Strategy 模式** — 封装平台特定行为: - -```text -IDesktopPropertyStrategy -├── IDesktopDisplaySizeStrategy ← 窗口尺寸与行为策略 -│ ├── WindowsDisplaySizePolicy ← 无边框、置底、避免系统 UI -│ └── LinuxWSLDisplaySizePolicy ← WSL 环境下的窗口策略 -└── (可扩展其他策略类型) -```text - -### 4.2 平台分发机制 - -每个平台目录提供两个分发函数,由公共 Helper 统一调度: - -```text -platform_helper.h ← 通用接口 - native_impl() → PlatformFactoryAPI(策略工厂) - native_display() → DisplayBackendFactoryAPI(后端工厂) - -display_backend_helper.h ← 显示后端专用接口 - native_display() → 转发到 native_display_impl() - native_display_impl() ← 每个平台实现此函数 -```text - -**调用链**: - -```text -CFDesktopEntity 构造 - │ - ├─ platform::native_display() - │ └─ native_display_impl() ← 平台分发 - │ ├─ Windows: new WindowsDisplayServerBackend - │ └─ WSL: new WSLDisplayServerBackend - │ - └─ DisplayServerBackendFactory::register_creator(creator, release) -```bash - -### 4.3 WSL 检测 - -**文件**: `cmake/env_check/detail_os_checker.cmake` - -三重检测机制(按优先级): - -1. **环境变量**: `WSL_DISTRO_NAME` 存在 -2. **内核版本**: `uname -r` 包含 `microsoft` 或 `wsl` -3. **procfs**: `/proc/version` 包含 `microsoft` 或 `wsl` - -检测到 WSL 后,CMake 设置 `IS_WSL=TRUE` 并定义 `CFDESKTOP_OS_WSL` 宏。 - ---- - -## 五、显示后端实现 - -### 5.1 后端对比 - -| 维度 | Windows 后端 | WSL/X11 后端 | -|------|-------------|-------------| -| **角色** | Compositor(伪桌面) | Compositor(伪桌面) | -| **运行模式** | 全屏覆盖层 | XWayland 客户端 | -| **窗口跟踪** | `SetWinEventHook` + `EnumWindows` | XCB `SubstructureNotify` + `xcb_query_tree` | -| **窗口封装** | `WindowsWindow(HWND)` | `WSLX11Window(xcb_window_t)` | -| **标题查询** | `GetWindowTextW` | `xcb_get_property(_NET_WM_NAME)` | -| **几何查询** | `GetWindowRect` | `xcb_get_geometry` + `xcb_translate_coordinates` | -| **关闭窗口** | `PostMessage(WM_CLOSE)` | `xcb_send_event(WM_DELETE_WINDOW)` | -| **事件集成** | `WINEVENT_OUTOFCONTEXT` → GUI 线程 | `QSocketNotifier` → Qt 事件循环 | -| **显示服务器** | Windows DWM | XWayland / Weston | +CFDesktop 的核心挑战是在 Windows 客户端、Linux X11/Wayland 合成器、嵌入式直接渲染等截然不同的运行场景中复用同一套 Shell / Panel / WindowManager 代码。为此,系统采用 **base -> ui -> desktop 三层单向依赖架构**:底层 `base` 提供硬件探测与基础工具,中层 `ui` 构建完整的 Material Design 3 设计系统(颜色、排版、形状、动效),顶层 `desktop` 组合两者实现桌面环境。严格单向依赖(`desktop` 可依赖 `ui` 和 `base`,反之不可)确保底层模块不感知上层业务,使每层可独立测试、独立替换。 -### 5.2 Windows 后端数据流 +平台差异通过 **Factory + Strategy 双模式抽象层** 屏蔽。`DisplayServerBackendFactory` 负责创建平台特定的显示后端对象,`IDesktopDisplaySizeStrategy` 封装平台特定的窗口行为策略。每个平台只需实现两个分发函数(`native_impl()` 和 `native_display()`),公共 Helper 统一调度,新增平台无需修改已有代码。 -```text -┌─────────────────────────────────────────────────────┐ -│ WindowsDisplayServerBackend │ -│ initialize(): │ -│ ├─ QApplication::instance() 检查 │ -│ ├─ WindowsWindowBackend::startTracking() │ -│ │ ├─ SetWinEventHook(CREATE/SHOW/DESTROY) │ -│ │ └─ EnumWindows() 扫描已有窗口 │ -│ └─ connect signals → externalWindowAppeared/... │ -├─────────────────────────────────────────────────────┤ -│ WindowsWindowBackend │ -│ WinEventProc (static callback) │ -│ ├─ EVENT_OBJECT_SHOW → onExternalWindowShown │ -│ │ ├─ shouldTrackWindow(hwnd) │ -│ │ │ ├─ IsWindow + IsWindowVisible │ -│ │ │ ├─ GetAncestor(GA_ROOT) == hwnd │ -│ │ │ ├─ GetWindowTextLength > 0 │ -│ │ │ ├─ PID != 自身 │ -│ │ │ └─ 排除 Progman/Shell_TrayWnd/WorkerW │ -│ │ └─ registerWindow(hwnd) → emit window_came │ -│ └─ EVENT_OBJECT_DESTROY → unregisterWindow │ -├─────────────────────────────────────────────────────┤ -│ WindowsWindow(HWND) │ -│ title() → GetWindowTextW │ -│ geometry() → GetWindowRect │ -│ set_geometry() → SetWindowPos │ -│ requestClose() → PostMessage(WM_CLOSE) │ -│ raise() → SetForegroundWindow + BringWindowToTop│ -└─────────────────────────────────────────────────────┘ -```text - -### 5.3 WSL/X11 后端数据流 - -```text -┌─────────────────────────────────────────────────────┐ -│ WSLDisplayServerBackend │ -│ initialize(): │ -│ ├─ connectToX11() │ -│ │ ├─ xcb_connect(nullptr, &screen_num) │ -│ │ ├─ xcb_get_setup → xcb_setup_roots_iterator │ -│ │ └─ 获取 root_window_ │ -│ ├─ WSLX11WindowBackend::startTracking(conn,root) │ -│ │ ├─ internAtoms() → 缓存 9 个 X11 Atom │ -│ │ ├─ xcb_change_window_attributes( │ -│ │ │ SUBSTRUCTURE_NOTIFY) │ -│ │ ├─ xcb_query_tree → 扫描已有窗口 │ -│ │ └─ QSocketNotifier(xcb_fd) → processEvents │ -│ └─ connect signals → externalWindowAppeared/... │ -├─────────────────────────────────────────────────────┤ -│ WSLX11WindowBackend │ -│ processXcbEvents() ← QSocketNotifier::activated │ -│ ├─ XCB_CREATE_NOTIFY │ -│ │ └─ shouldTrackWindow → registerWindow │ -│ ├─ XCB_MAP_NOTIFY │ -│ │ └─ shouldTrackWindow → registerWindow │ -│ ├─ XCB_DESTROY_NOTIFY → unregisterWindow │ -│ ├─ XCB_CONFIGURE_NOTIFY → geometryChanged signal │ -│ └─ XCB_PROPERTY_NOTIFY → titleChanged signal │ -│ │ -│ shouldTrackWindow(win): │ -│ ├─ xcb_get_window_attributes → viewable? │ -│ ├─ !override_redirect │ -│ ├─ _NET_WM_NAME / WM_NAME 非空 │ -│ ├─ _NET_WM_PID != 自身 PID │ -│ └─ _NET_WM_WINDOW_TYPE != DOCK/DESKTOP │ -├─────────────────────────────────────────────────────┤ -│ WSLX11Window(xcb_window_t) │ -│ title() → xcb_get_property(_NET_WM_NAME) │ -│ → fallback: WM_NAME │ -│ geometry() → xcb_get_geometry │ -│ + xcb_translate_coordinates → 根坐标 │ -│ set_geometry() → xcb_configure_window │ -│ requestClose() → WM_DELETE_WINDOW ClientMessage │ -│ → fallback: xcb_kill_client │ -│ raise() → xcb_configure_window(ABOVE) │ -└─────────────────────────────────────────────────────┘ -```bash - -### 5.4 未来后端规划 +初始化采用 **DAG 阶段链**(`RunEarlyInit` -> `RunStageInit` -> `boot_desktop` -> `exec`),而非扁平的 init 列表。阶段之间有明确的数据依赖:早期初始化建立日志和配置,阶段初始化完成系统检测和资源预加载,最后才创建 Qt Widget 和平台后端。`CFDesktopEntity` 作为中央单例管理整个生命周期,其 `release()` 必须在 `QApplication` 存活时调用(内部持有 QWidget 实例)。 -| 后端 | 角色 | 关键技术 | 状态 | -|------|------|---------|------| -| X11 窗口管理器 | Compositor | XCB SubstructureRedirect + EWMH + XComposite | 规划中 | -| Wayland 合成器 | Compositor | QtWaylandCompositor + xdg-shell + DRM/KMS | 规划中 | -| EGLFS | DirectRender | Qt EGLFS + libdrm/libgbm | 规划中 | -| LinuxFB | DirectRender | Qt linuxfb + evdev | 规划中 | - ---- +## 关键决策 -## 六、UI 框架分层 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 三层单向依赖(base/ui/desktop) | 防止循环依赖,每层可独立编译测试,底层不感知上层业务 | 扁平单层结构(耦合严重,无法独立测试) | +| Factory + Strategy 平台抽象 | 新增平台只实现接口,不修改已有代码;运行时可查询能力做降级 | 大量 `#ifdef` 平台分支(维护困难,违反开闭原则) | +| UI 五层流水线(数学->主题->动画->行为->控件) | MD3 设计系统本身分层清晰,逐层构建避免跨层抽象泄漏 | 单体 Widget 直接硬编码样式(无法换肤,无法复用行为) | +| DAG 阶段初始化链 | 阶段间有硬数据依赖(日志先于检测,检测先于 Widget 创建),错误可精确定位到阶段 | 随意顺序初始化(隐式依赖导致启动失败难以调试) | +| `--whole-archive` 共享库构建 | 工厂自动注册函数可能不被直接引用,whole-archive 确保符号不丢失 | 手动注册每个工厂(易遗漏,违背自动化设计) | +| `CFDesktopEntity` 单例生命周期管理 | 集中管理 Widget/后端/策略的析构顺序,保证 Qt 对象树正确销毁 | 分散在各模块各自清理(销毁顺序不可控,UAF 风险) | -UI 模块采用五层架构,从底层数学工具到顶层控件,逐层构建 Material Design 3 设计系统: +## 模块层次 ```text -┌─────────────────────────────────────────────────┐ -│ Layer 5: Widget 适配层 (cf_ui_widget) │ -│ Button, TextField, Switch, TabView... │ -│ (20+ 生产级 Widget,面向开发者) │ -├─────────────────────────────────────────────────┤ -│ Layer 4: Material 行为层 (cf_ui_components) │ -│ StateMachine, Ripple, Elevation, FocusRing │ -│ (MD3 交互行为:水波纹、状态机、焦点) │ -│ + Material 动画实现 │ -│ (Fade, Slide, Scale, Spring) │ -├─────────────────────────────────────────────────┤ -│ Layer 3: 动画引擎层 (cf_ui_components) │ -│ Animation, AnimationFactory, Timing/Spring │ -│ (动画基础设施:时间线、弹性物理、工厂) │ -├─────────────────────────────────────────────────┤ -│ Layer 2: 主题引擎层 (cf_ui_core) │ -│ ThemeManager, Token 系统 │ -│ (MD3 Token: 颜色、排版、形状、动效) │ -├─────────────────────────────────────────────────┤ -│ Layer 1: 数学与工具层 (cf_ui_base) │ -│ Color, Math, Geometry, Easing, DevicePixel │ -│ (基础数学:HCT 色彩、easing 曲线、DPI) │ -└─────────────────────────────────────────────────┘ -```yaml - -### 各层详情 - -**Layer 1 — 数学与工具层** (`ui/base/`) -- `color.h`: HCT 色彩系统(Hue-Chroma-Tone) -- `math_helper.h`: 数学辅助函数 -- `geometry_helper.h`: 几何计算 -- `easing.h`: 缓动曲线(Material Motion 标准) -- `device_pixel.h`: DPI 适配与设备像素比 - -**Layer 2 — 主题引擎层** (`ui/core/`) -- `theme_manager.h`: 主题管理器,动态切换主题 -- `token.hpp`: 设计 Token 系统(~35KB),包含完整的 MD3 Token 定义 -- `color_scheme.h`: 颜色方案(Light/Dark/动态) -- `font_type.h`: 排版系统 -- `motion_spec.h`: 动效规格 - -**Layer 3 — 动画引擎层** (`ui/components/`) -- `animation.h`: 动画基类 -- `animation_factory_manager.h`: 动画工厂管理器 -- `timing_animation.h`: 基于时间线的动画 -- Material 动画实现:Fade, Slide, Scale, Spring - -**Layer 4 — Material 行为层** (`ui/widget/material/base/`) -- `state_machine.h`: Widget 状态机(Pressed/Hovered/Focused/Disabled) -- `ripple_helper.h`: Material 水波纹效果 -- `elevation_controller.h`: Z 轴高度管理(阴影层次) -- `focus_ring.h`: 焦点指示环 - -**Layer 5 — Widget 适配层** (`ui/widget/material/widget/`) -- 20+ 生产级 Widget:Button, Checkbox, TextField, Slider, Switch, ComboBox, ListView, TableView, TreeView, TabView, ScrollView 等 - ---- +desktop/ (Layer 3) -- Shell, Panel, 后端, 初始化 + | +ui/ (Layer 2) -- MD3 组件, 主题, 动画, 20+ Widget + | +base/ (Layer 1) -- CPU/GPU/Memory 探测, 工具库, 工厂基础设施 + | +Qt 6 / OS API +``` -## 七、初始化与生命周期 - -### 7.1 完整启动流程 - -```cpp -main(argc, argv) -│ -├─ QApplication cf_desktop_app(argc, argv) ← Qt 应用创建 -│ -└─ run_desktop_session() ← DLL 入口 - │ - ├─ init_session::RunEarlyInit() ← 早期初始化 - │ └─ 日志、路径、配置 - │ - ├─ init_session::RunStageInit() ← 阶段初始化 - │ └─ 系统检测、资源预加载 - │ - ├─ boot_desktop() ← 桌面启动 - │ │ - │ ├─ CFDesktopEntity::instance() ← 创建单例 - │ │ ├─ new CFDesktop(this) ← 创建桌面 Widget - │ │ ├─ new PlatformFactory ← 创建策略工厂 - │ │ │ - │ │ └─ platform::native_display() ← 注册显示后端 - │ │ └─ DisplayServerBackendFactory::register_creator() - │ │ - │ ├─ CFDesktopEntity::run_init() - │ │ ├─ 创建 IDesktopDisplaySizeStrategy - │ │ ├─ CFDesktopWindowProxy::set_window_display_strategy() - │ │ ├─ DisplayServerBackendFactory::make_unique() ← 创建后端 - │ │ │ └─ native_display_impl() - │ │ │ ├─ Windows: new WindowsDisplayServerBackend - │ │ │ └─ WSL: new WSLDisplayServerBackend - │ │ │ - │ │ ├─ display_backend_->initialize() - │ │ │ ├─ connectToX11() / Win32 初始化 - │ │ │ ├─ window_backend_->startTracking() - │ │ │ └─ connect signals - │ │ │ - │ │ └─ connect window_came/gone → 日志 - │ │ - │ ├─ CFDesktopWindowProxy::show_desktop() ← 显示桌面窗口 - │ └─ init_session::ReleaseStageInitOldResources() - │ - ├─ QApplication::exec() ← 事件循环 - │ - ├─ CFDesktopEntity::release() ← 清理(QApplication 存活时) - └─ Logger::flush_sync() ← 刷日志 -```text - -### 7.2 CFDesktopEntity 生命周期 - -`CFDesktopEntity` 是桌面环境的中央单例,管理整个生命周期: - -```text -构造 ──────────────────────────────────────── 析构 - │ │ - ├─ CFDesktop* (Widget) ├─ stopTracking() - ├─ PlatformFactory (策略) ├─ xcb_disconnect() - ├─ DisplayServerBackend (后端) └─ 清理 Widget - │ └─ IWindowBackend (窗口跟踪) - │ └─ IWindow[] (窗口列表) - │ - └─ run_init() 初始化全部组件 -```yaml - -**关键约束**:`CFDesktopEntity::release()` 必须在 `QApplication` 存活时调用,因为其内部持有 `QWidget` 实例。 - ---- - -## 八、构建系统概览 - -### 8.1 顶层 CMake 结构 - -```cmake -project(CFDesktop VERSION 0.13.1) - -# 预配置:WSL 检测、构建类型、Qt 设置 -include(cmake/check_pre_configure.cmake) -include(cmake/generate_meta_info.cmake) - -# 三大模块,按依赖顺序添加 -add_subdirectory(base) # 1. 基础库 -add_subdirectory(ui) # 2. UI 框架(依赖 base) -add_subdirectory(desktop) # 3. 桌面实现(依赖 base + ui) - -# 辅助模块 -add_subdirectory(example) # 示例应用 -add_subdirectory(test) # 测试套件 -```bash - -### 8.2 条件编译 - -| 条件 | 效果 | -|------|------| -| `WIN32` | 编译 Windows 平台代码,链接 Win32 系统库 | -| `UNIX AND NOT APPLE` + `IS_WSL` | 编译 Linux/WSL 平台代码,链接 XCB | -| `CFDESKTOP_OS_WSL` | C++ 宏,标记 WSL 环境 | -| `CFDESKTOP_HAS_XCB` | C++ 宏,标记 XCB 可用 | - -### 8.3 共享库构建技巧 - -所有静态库通过 `--whole-archive` 链接到 `CFDesktop_shared`,确保即使没有被直接引用的符号(如工厂注册函数)也被包含: - -```cmake -target_link_libraries(CFDesktop_shared PRIVATE - "-Wl,--whole-archive" - ${CFDESKTOP_STATIC_LIBS} - "-Wl,--no-whole-archive" -) -```cmake - -最终可执行文件 `CFDesktop` 仅包含 `main.cpp`,链接 `CFDesktop_shared`。 - ---- - -## 九、WSL/X11 后端文件清单 - -本次实现新增的 WSL X11 后端文件: - -| 文件路径 | 职责 | -|----------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window.h` | `WSLX11Window` — IWindow 的 XCB 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window.cpp` | 标题查询、几何操作、关闭/置顶 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h` | `WSLX11WindowBackend` — IWindowBackend 的 XCB 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp` | Atom 缓存、事件监听、窗口过滤、信号发射 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h` | `WSLDisplayServerBackend` — IDisplayServerBackend 实现 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp` | XCB 连接管理、初始化/关闭、信号转发 | - -修改的现有文件: - -| 文件路径 | 变更 | -|----------|------| -| `desktop/ui/platform/linux_wsl/linux_wsl_platform.cpp` | `native_display_impl()` 从返回 nullptr 改为返回后端创建函数 | -| `desktop/ui/platform/CMakeLists.txt` | 添加 XCB 依赖(`pkg_check_modules` + `target_link_libraries`) | - ---- +## 当前状态 -*本文档随项目开发持续更新。* +已实现。详细接口规格参见 HandBook,构建产物与 CMake 目标参见 CLAUDE.md 模块地图。 diff --git a/document/development/README.md b/document/development/README.md deleted file mode 100644 index f894365ae..000000000 --- a/document/development/README.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -title: CFDesktop 开发环境文档 -description: 欢迎使用 CFDesktop 开发环境设置指南。本文档系列将帮助您搭建完整的开发环境,从基础工具安装 ---- - -# CFDesktop 开发环境文档 - -欢迎使用 CFDesktop 开发环境设置指南。本文档系列将帮助您搭建完整的开发环境,从基础工具安装到高级配置,逐步引导您成为 CFDesktop 开发者。 - ---- - -## 项目简介 - -**CFDesktop** 是一个基于 Qt6 的现代化嵌入式桌面框架,旨在为各种嵌入式设备提供统一、现代化的桌面环境。 - -### 核心特性 - -- **跨平台支持**: Windows 10/11、Ubuntu 22.04+、Debian 12+ -- **多架构支持**: x86_64、ARM64、ARMhf -- **性能自适应**: 根据设备硬件能力自动调整 UI 特效和功能 -- **Material Design 3**: 完整实现的现代化 UI 组件库 -- **模块化设计**: 松耦合架构,便于裁剪和定制 - -### 技术栈 - -| 技术 | 版本 | 用途 | -|:---|:---:|:---| -| **C++** | C++23 | 核心开发语言 | -| **Qt** | 6.8.3+ | UI 框架 | -| **CMake** | 3.16+ | 构建系统 | -| **Docker** | 最新 | 多架构构建验证 | -| **Git** | 最新 | 版本控制 | - ---- - -## 文档导航 - -| 文档 | 内容 | 预计时间 | -|:---|:---|:---:| -| [01. 前置要求](01_prerequisites.md) | 硬件要求、操作系统支持、必需软件安装 | 15-30 分钟 | -| [02. 快速开始](02_quick_start.md) | 最快速的方式启动项目 | 5-10 分钟 | -| [04. 开发工具](04_development_tools.md) | 代码格式化、静态分析、调试工具 | 10-15 分钟 | -| [05. Docker 构建](05_docker_build.md) | Docker 多架构构建指南 | 15-20 分钟 | -| [06. Git Hooks](06_git_hooks.md) | Pre-commit 和 Pre-push Hook 使用说明 | 10-15 分钟 | -| [07. 常见问题](07_troubleshooting.md) | 问题排查和解决方案 | - | - ---- - -## 环境要求速览 - -### 硬件要求 - -| 组件 | 最低配置 | 推荐配置 | -|:---|:---:|:---:| -| **CPU** | 4 核心 | 8 核心以上 | -| **RAM** | 8GB | 16GB 或更多 | -| **硬盘** | 20GB 可用空间 | 50GB+ SSD | - -### 操作系统支持 - -| 平台 | 支持版本 | 工具链 | -|:---|:---|:---| -| **Windows** | Windows 10/11 | MinGW 或 LLVM | -| **Linux** | Ubuntu 22.04+, Debian 12+ | GCC 或 Clang | - -### 必需软件 - -| 软件 | 最低版本 | 推荐版本 | -|:---|:---:|:---:| -| **Docker Desktop** | 最新稳定版 | 最新版 | -| **Git** | 2.30+ | 最新版 | -| **VSCode** | (推荐) 最新版 | 最新版 | -| **Qt6** | 6.8.3 | 6.8.3+ | -| **CMake** | 3.16 | 3.20+ | -| **Python** | 3.8+ | 3.10+ (用于 aqtinstall) | - ---- - -## 推荐开发流程 - -```mermaid -graph LR - A[1. 环境准备] --> B[2. 克隆项目] - B --> C[3. 配置 Qt6] - C --> D[4. 首次构建] - D --> E[5. 运行测试] - E --> F[6. 开始开发] - - style A fill:#4CAF50 - style B fill:#2196F3 - style C fill:#9C27B0 - style D fill:#FF9800 - style E fill:#FF5722 - style F fill:#9E9E9E -```text - -### 快速开始 - -```bash -# 1. 克隆项目 -git clone https://github.com/your-org/CFDesktop.git -cd CFDesktop - -# 2. Windows 快速构建 -.\scripts\build_helpers\windows_fast_develop_build.ps1 - -# 3. Linux 快速构建 -./scripts/build_helpers/linux_fast_develop_build.sh -```yaml - ---- - -## 下一步 - -请按照文档顺序阅读: - -1. **[01. 前置要求](01_prerequisites.md)** - 确保您的开发环境满足所有要求 -2. **[02. 快速开始](02_quick_start.md)** - 快速上手开发 -3. **[03. 构建系统](03_build_system.md)** - 了解 CMake 构建系统 -4. **[04. 开发工具](04_development_tools.md)** - 配置您喜欢的开发工具 - ---- - -## 获取帮助 - -### 问题反馈 - -如果您在环境设置过程中遇到问题: - -- **GitHub Issues**: [提交问题](https://github.com/your-org/CFDesktop/issues) -- **讨论区**: [GitHub Discussions](https://github.com/your-org/CFDesktop/discussions) -- **文档**: 查看项目根目录下的 [README](../../) - -### 常见问题 - -**Q: 必须使用 Docker 吗?** - -A: 不是必须的,但推荐使用 Docker 进行多架构构建验证。本地开发可以直接使用 Qt6 和 CMake。 - -**Q: 可以使用其他 IDE 吗?** - -A: 可以。项目主要配置 VSCode + Clangd,但也支持 QtCreator 和其他支持 CMake 的 IDE。 - -**Q: Windows 下推荐使用 MinGW 还是 LLVM?** - -A: 两者都支持。LLVM/Clang 通常有更好的兼容性和错误信息,MinGW 则更轻量。 - ---- - -## 附录 - -### 相关链接 - -- [Qt6 官方文档](https://doc.qt.io/qt-6/) -- [CMake 官方文档](https://cmake.org/documentation/) -- [Docker 官方文档](https://docs.docker.com/) -- [aqtinstall 文档](https://aqtinstall.readthedocs.io/) - -### 文档更新 - -- **版本**: 0.13.1 -- **最后更新**: 2026-03-30 -- **维护者**: CFDesktop 开发团队 - ---- - -
- - [返回项目首页](../index.md) | [前置要求 →](01_prerequisites.md) - - **CFDesktop** - 为嵌入式设备打造的现代化桌面框架 - -
diff --git a/document/development/index.md b/document/development/index.md index dce85f3f7..df7fb0118 100644 --- a/document/development/index.md +++ b/document/development/index.md @@ -1,6 +1,6 @@ --- title: 开发指南 -description: 本节面向重新启动开发时的日常入口。,1. 当前项目状态 +description: 本节面向重新启动开发时的日常入口。 --- # 开发指南 @@ -33,4 +33,46 @@ QT_QPA_PLATFORM=offscreen ctest --test-dir out/build_develop/test --output-on-fa pnpm install pnpm dev pnpm build -```text +``` + +## 环境要求速览 + +### 硬件要求 + +| 组件 | 最低配置 | 推荐配置 | +|:---|:---:|:---:| +| **CPU** | 4 核心 | 8 核心以上 | +| **RAM** | 8GB | 16GB 或更多 | +| **硬盘** | 20GB 可用空间 | 50GB+ SSD | + +### 操作系统支持 + +| 平台 | 支持版本 | 工具链 | +|:---|:---|:---| +| **Windows** | Windows 10/11 | MinGW 或 LLVM | +| **Linux** | Ubuntu 22.04+, Debian 12+ | GCC 或 Clang | + +### 必需软件 + +| 软件 | 最低版本 | 推荐版本 | +|:---|:---:|:---:| +| **Docker Desktop** | 最新稳定版 | 最新版 | +| **Git** | 2.30+ | 最新版 | +| **VSCode** | (推荐) 最新版 | 最新版 | +| **Qt6** | 6.8.3 | 6.8.3+ | +| **CMake** | 3.16 | 3.20+ | +| **Python** | 3.8+ | 3.10+ (用于 aqtinstall) | + +## 快速克隆与构建 + +```bash +# 1. 克隆项目 +git clone https://github.com/Charliechen114514/CFDesktop.git +cd CFDesktop + +# 2. Windows 快速构建 +.\scripts\build_helpers\windows_fast_develop_build.ps1 + +# 3. Linux 快速构建 +./scripts/build_helpers/linux_fast_develop_build.sh +``` diff --git a/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md b/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md index a1443c493..2c65840c5 100644 --- a/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md +++ b/document/notes/01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md @@ -1,1109 +1,27 @@ --- title: 桌面行为建模:从 bool 到 QFlags -description: 1. 问题背景:为什么 struct bool 不够用 +description: 用 QFlags 替代 struct bool 字段,为窗口行为建模 --- -# 桌面行为建模:从 bool 到 QFlags +# 桌面行为建模:从 bool 到 QFlags -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [问题背景:为什么 struct bool 不够用](#问题背景为什么-struct-bool-不够用) -2. [Flag 模型的引入与核心思想](#flag-模型的引入与核心思想) -3. [Qt QFlags 深度解析](#qt-qflags-深度解析) -4. [工程化实践范式](#工程化实践范式) -5. [设计原则与最佳实践](#设计原则与最佳实践) -6. [常见陷阱与解决方案](#常见陷阱与解决方案) -7. [与其他方案的对比](#与其他方案的对比) +CFDesktop 需要描述窗口的多种行为特性(全屏、无边框、置底、可调整大小等)。最初考虑使用 `struct { bool fullscreen; bool frameless; ... }` 的朴素方案,但该方案存在根本缺陷:字段不可通过位运算组合、每次扩展字段都破坏 ABI、内存占用高(N 字节 vs 1-4 字节)、且无法与 Qt 元对象系统集成。 ---- - -## 问题背景:为什么 struct bool 不够用 - -### 传统 bool 结构体方案 - -在桌面应用程序开发中,我们经常需要描述窗口的各种行为特性。最直观的实现方式是使用包含多个布尔字段的结构体: - -```cpp -// ❌ 传统的 bool 结构体方案 -struct DesktopBehavior { - bool fullscreen; // 是否全屏 - bool frameless; // 是否无边框 - bool stayOnTop; // 是否置顶 - bool stayOnBottom; // 是否置底 - bool allowResize; // 是否允许调整大小 - bool avoidSystemUI; // 是否避开系统UI - bool transparent; // 是否透明 - bool clickThrough; // 是否点击穿透 -}; -```text - -### 方案缺陷分析 - -这种朴素的实现方式存在以下严重问题: - -#### 1. 内存效率低下 - -```cpp -sizeof(DesktopBehavior) // 通常为 8 × bool = 8 字节(甚至更多由于对齐) -```text - -虽然 8 字节看起来不大,但当我们需要存储大量行为配置时,这种内存开销会变得显著。更重要的是,bool 类型的操作通常不是原子的,在多线程环境下需要额外的同步机制。 - -#### 2. 不可组合性 - -```cpp -// ❌ 无法方便地组合行为 -DesktopBehavior b1 = {true, false, false, false, false, false, false, false}; -DesktopBehavior b2 = {false, true, false, false, false, false, false, false}; - -// 如何得到"全屏且无边框"的行为? -// 需要逐字段手动合并,繁琐且容易出错 -```text - -#### 3. 扩展性差 - -每次添加新的行为特性都需要: - -1. 修改结构体定义(破坏 ABI 兼容性) -2. 更新所有初始化代码 -3. 修改序列化/反序列化逻辑 -4. 调整所有相关的比较和复制操作 - -```cpp -// 添加新字段需要修改所有现有代码 -struct DesktopBehavior { - // ... 原有字段 - bool newFeature; // 新增字段 -}; -```text - -#### 4. 不适合策略系统 - -在策略模式中,我们经常需要: - -- 将多个行为组合成一个能力集合 -- 快速判断是否具备某个能力 -- 高效地传递行为配置 - -bool 结构体在这些场景下表现不佳: - -```cpp -// ❌ 判断能力繁琐 -bool hasFullscreen(const DesktopBehavior& b) { - return b.fullscreen; -} - -// ❌ 组合能力需要逐字段操作 -DesktopBehavior combine(const DesktopBehavior& a, const DesktopBehavior& b) { - return { - a.fullscreen || b.fullscreen, - a.frameless || b.frameless, - // ... 需要处理每个字段 - }; -} -```text - -### 更好的方案:Bitmask 模型 - -计算机科学中,处理多个独立布尔值的经典方法是使用位掩码(Bitmask): - -```text -位 0: Fullscreen -位 1: Frameless -位 2: StayOnTop -位 3: StayOnBottom -位 4: AllowResize -位 5: AvoidSystemUI -... -```yaml - -这种方式的优势: - -- **紧凑存储**:8 个行为只需 1 个字节(或更少的位数) -- **原生支持组合**:使用位运算符 `|`、`&`、`^` -- **高效查询**:使用位掩码和 `&` 操作符 -- **可扩展**:新增行为只需新增位值 - ---- - -## Flag 模型的引入与核心思想 - -### 基本概念 - -Flag 模型的核心思想是: - -> **行为 = 多个独立标志位的组合** - -每个标志位(Flag)代表一个独立的行为特性,多个标志位可以通过位运算组合成一个完整的行为描述。 - -### C++ 枚举作为标志位 - -使用枚举类型定义标志位是最常见的做法: - -```cpp -// ✅ 使用枚举定义行为标志 -enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, // 二进制: 00000001 - FramelessFlag = 1 << 1, // 二进制: 00000010 - StayOnTopFlag = 1 << 2, // 二进制: 00000100 - StayOnBottomFlag = 1 << 3, // 二进制: 00001000 - AllowResizeFlag = 1 << 4, // 二进制: 00010000 - AvoidSystemUIFlag = 1 << 5, // 二进制: 00100000 - TransparentFlag = 1 << 6, // 二进制: 01000000 - ClickThroughFlag = 1 << 7, // 二进制: 10000000 -}; -```text - -### 标志位组合 - -```cpp -// ✅ 使用位运算符组合标志位 -unsigned int behaviors = FullscreenFlag | FramelessFlag; -// 结果: 00000011 (同时具备全屏和无边框特性) - -// 添加更多特性 -behaviors |= StayOnTopFlag; -// 结果: 00000111 (全屏 + 无边框 + 置顶) -```text - -### 标志位测试 - -```cpp -// ✅ 使用位运算测试特性 -bool isFullscreen = (behaviors & FullscreenFlag) != 0; -bool isFrameless = (behaviors & FramelessFlag) != 0; -```text - -### 标志位移除 - -```cpp -// ✅ 使用位运算移除特性 -behaviors &= ~FullscreenFlag; // 移除全屏特性 -// 结果: 00000110 -```yaml - ---- - -## Qt QFlags 深度解析 - -Qt 框架提供了 `QFlags` 模板类,这是一个类型安全的位标志实现,被广泛应用于 Qt 的各个模块中。 - -### 官方文档定义 - -根据 [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html): - -> "The QFlags class provides a type-safe way of storing OR-combinations of enum values." - -### QFlags 的核心特性 - -#### 1. 类型安全 - -与裸 `unsigned int` 相比,`QFlags` 提供了编译时类型检查: - -```cpp -// ❌ 裸整数实现 - 无类型检查 -unsigned int behaviors = FullscreenFlag | 123; // 编译通过,但语义错误 - -// ✅ QFlags 实现 - 有类型检查 -QFlags behaviors = FullscreenFlag | FramelessFlag; -// behaviors = FullscreenFlag | 123; // 编译错误 -```text - -#### 2. 运算符友好 - -`QFlags` 重载了所有必要的位运算符: - -```cpp -QFlags b1 = FullscreenFlag | FramelessFlag; -QFlags b2 = StayOnTopFlag; - -// 位或(组合) -auto combined = b1 | b2; - -// 位与(交集) -auto intersection = b1 & b2; - -// 异或(对称差) -auto difference = b1 ^ b2; - -// 取反 -auto inverted = ~b1; - -// 复合赋值 -b1 |= b2; -b1 &= b2; -b1 ^= b2; -```text - -#### 3. QVariant / MetaObject 兼容 - -通过 Qt 的元对象系统,`QFlags` 可以与 `QVariant` 无缝集成: - -```cpp -QVariant var = QVariant::fromValue(fullscreen | frameless); -auto flags = var.value>(); -```text - -### QFlags 声明宏 - -Qt 提供了两个重要的宏来简化 `QFlags` 的使用: - -#### Q_DECLARE_FLAGS - -```cpp -// 在类中声明标志类型 -class DesktopWindow { -public: - enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, - FramelessFlag = 1 << 1, - // ... - }; - - Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - // 等价于: typedef QFlags DesktopBehaviors; -}; -```text - -#### Q_DECLARE_OPERATORS_FOR_FLAGS - -```cpp -// 在类外声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopWindow::DesktopBehaviors) -```text - -这个宏为标志类型声明全局的 `operator|()` 函数,使得: - -```cpp -DesktopWindow::DesktopBehaviors behaviors = - DesktopWindow::FullscreenFlag | DesktopWindow::FramelessFlag; -```text - -### testFlag() 方法 - -`QFlags` 提供了便捷的 `testFlag()` 方法用于测试标志位: - -```cpp -DesktopBehaviors behaviors = FullscreenFlag | FramelessFlag; - -// ✅ 使用 testFlag() -if (behaviors.testFlag(FullscreenFlag)) { - // ... -} - -// 等价于: -if ((behaviors & FullscreenFlag) != 0) { - // ... -} -```text - -### Q_ENUM 与 Q_FLAGS 集成 - -从 Qt 5.5 开始,可以使用 `Q_ENUM` 和 `Q_FLAGS` 宏将枚举和标志注册到元对象系统: - -```cpp -class DesktopWindow : public QObject { - Q_OBJECT -public: - enum DesktopBehaviorFlag { - FullscreenFlag = 1 << 0, - FramelessFlag = 1 << 1, - // ... - }; - Q_ENUM(DesktopBehaviorFlag) - Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - Q_FLAGS(DesktopBehaviors) -}; -```text - -这样做的优势: - -1. **QML 互操作**:可以在 QML 中使用这些枚举和标志 -2. **字符串转换**:`QMetaEnum` 提供枚举值与字符串的相互转换 -3. **调试支持**:调试器可以显示符号名称而不是数字值 - -### 完整示例 - -```cpp -#include - -// 1. 定义枚举 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, -}; - -// 2. 声明标志类型 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 3. 声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -// 4. 使用 -int main() { - // 创建行为组合 - DesktopBehaviors behaviors = DesktopBehaviorFlag::Fullscreen - | DesktopBehaviorFlag::Frameless - | DesktopBehaviorFlag::StayOnTop; - - // 测试标志位 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - qDebug() << "Fullscreen enabled"; - } - - // 添加标志位 - behaviors |= DesktopBehaviorFlag::AllowResize; - - // 移除标志位 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - - // 检查是否有任何标志位 - if (behaviors != DesktopBehaviorFlag::None) { - qDebug() << "Has behaviors"; - } - - return 0; -} -```yaml - ---- - -## 工程化实践范式 - -### 基础定义 - -```cpp -// DesktopBehavior.h -#pragma once -#include - -namespace desktop { - -// 行为标志枚举 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - Modal = 1 << 8, - Popup = 1 << 9, - Tool = 1 << 10, - Splash = 1 << 11, -}; - -// 声明标志类型 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 声明运算符 -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -} // namespace desktop -```text - -### 常用预定义组合 - -```cpp -// DesktopBehavior.h -namespace desktop { - -// 常用行为组合 -constexpr auto NormalBehavior = DesktopBehaviorFlag::None; -constexpr auto FullscreenBehavior = DesktopBehaviorFlag::Fullscreen; -constexpr auto FramelessBehavior = DesktopBehaviorFlag::Frameless; -constexpr auto AlwaysOnTopBehavior = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::Frameless; -constexpr auto WidgetBehavior = DesktopBehaviorFlag::Tool | DesktopBehaviorFlag::Frameless | DesktopBehaviorFlag::StayOnTop; -constexpr auto SplashBehavior = DesktopBehaviorFlag::Splash | DesktopBehaviorFlag::Frameless | DesktopBehaviorFlag::StayOnTop; - -} // namespace desktop -```text - -### 行为查询接口 - -```cpp -// DesktopBehaviorQuery.h -#pragma once -#include "DesktopBehavior.h" -#include - -namespace desktop { - -// 行为查询接口 -class IDesktopBehaviorQuery { -public: - virtual ~IDesktopBehaviorQuery() = default; - - // 查询所有行为 - virtual DesktopBehaviors behaviors() const = 0; - - // 查询单个行为 - virtual bool hasBehavior(DesktopBehaviorFlag flag) const = 0; - - // 查询行为组合 - virtual bool hasAnyBehavior(DesktopBehaviors flags) const = 0; - virtual bool hasAllBehaviors(DesktopBehaviors flags) const = 0; - - // 行为描述(用于日志/调试) - virtual QString behaviorDescription() const = 0; -}; - -// 默认实现 -class DesktopBehaviorQuery : public IDesktopBehaviorQuery { -public: - explicit DesktopBehaviorQuery(DesktopBehaviors behaviors) - : m_behaviors(behaviors) {} - - DesktopBehaviors behaviors() const override { - return m_behaviors; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return m_behaviors.testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (m_behaviors & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (m_behaviors & flags) == flags; - } - - QString behaviorDescription() const override { - QStringList flags; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) - flags << "Fullscreen"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags << "Frameless"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags << "StayOnTop"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags << "StayOnBottom"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) - flags << "AllowResize"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::AvoidSystemUI)) - flags << "AvoidSystemUI"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Transparent)) - flags << "Transparent"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::ClickThrough)) - flags << "ClickThrough"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Modal)) - flags << "Modal"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Popup)) - flags << "Popup"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Tool)) - flags << "Tool"; - if (m_behaviors.testFlag(DesktopBehaviorFlag::Splash)) - flags << "Splash"; - - return flags.isEmpty() ? "None" : flags.join(", "); - } - -private: - DesktopBehaviors m_behaviors; -}; - -} // namespace desktop -```text - -### 行为修改接口 - -```cpp -// DesktopBehaviorModifier.h -#pragma once -#include "DesktopBehavior.h" -#include - -namespace desktop { - -// 行为修改接口 -class IDesktopBehaviorModifier { -public: - virtual ~IDesktopBehaviorModifier() = default; - - // 设置行为 - virtual void setBehavior(DesktopBehaviorFlag flag, bool enabled = true) = 0; - - // 批量设置行为 - virtual void setBehaviors(DesktopBehaviors flags) = 0; - - // 添加行为(不覆盖现有行为) - virtual void addBehavior(DesktopBehaviorFlag flag) = 0; - virtual void addBehaviors(DesktopBehaviors flags) = 0; - - // 移除行为 - virtual void removeBehavior(DesktopBehaviorFlag flag) = 0; - virtual void removeBehaviors(DesktopBehaviors flags) = 0; - - // 切换行为 - virtual void toggleBehavior(DesktopBehaviorFlag flag) = 0; - - // 清除所有行为 - virtual void clearBehaviors() = 0; -}; - -// 默认实现 -class DesktopBehaviorModifier : public IDesktopBehaviorModifier { -public: - explicit DesktopBehaviorModifier(DesktopBehaviors initialBehaviors = DesktopBehaviorFlag::None) - : m_behaviors(initialBehaviors) {} - - void setBehavior(DesktopBehaviorFlag flag, bool enabled) override { - if (enabled) { - m_behaviors |= flag; - } else { - m_behaviors &= ~flag; - } - } - - void setBehaviors(DesktopBehaviors flags) override { - m_behaviors = flags; - } - - void addBehavior(DesktopBehaviorFlag flag) override { - m_behaviors |= flag; - } - - void addBehaviors(DesktopBehaviors flags) override { - m_behaviors |= flags; - } - - void removeBehavior(DesktopBehaviorFlag flag) override { - m_behaviors &= ~flag; - } - - void removeBehaviors(DesktopBehaviors flags) override { - m_behaviors &= ~flags; - } - - void toggleBehavior(DesktopBehaviorFlag flag) override { - m_behaviors ^= flag; - } - - void clearBehaviors() override { - m_behaviors = DesktopBehaviorFlag::None; - } - - DesktopBehaviors behaviors() const { - return m_behaviors; - } - -private: - DesktopBehaviors m_behaviors; -}; - -} // namespace desktop -```text - -### 与 QWidget 集成 - -```cpp -// DesktopWindowBehavior.h -#pragma once -#include "DesktopBehavior.h" -#include "DesktopBehaviorQuery.h" -#include "DesktopBehaviorModifier.h" -#include - -namespace desktop { - -// QWidget 行为扩展 -class DesktopWindowBehavior : public QWidget, public IDesktopBehaviorQuery { -public: - explicit DesktopWindowBehavior(QWidget* parent = nullptr) - : QWidget(parent) {} - - // 从当前窗口状态查询行为 - DesktopBehaviors behaviors() const override { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - if (isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - if (windowFlags() & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (windowFlags() & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (windowFlags() & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - - // 推断行为 - QSize minSize = minimumSize(); - QSize maxSize = maximumSize(); - if (minSize.isEmpty() && maxSize.isEmpty()) - result |= DesktopBehaviorFlag::AllowResize; - - return result; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return behaviors().testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (behaviors() & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (behaviors() & flags) == flags; - } - - QString behaviorDescription() const override { - return DesktopBehaviorQuery(behaviors()).behaviorDescription(); - } - - // 应用行为到窗口 - void applyBehaviors(DesktopBehaviors behaviors) { - Qt::WindowFlags flags = windowFlags(); - - // 清除相关标志 - flags &= ~(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowStaysOnBottomHint); - - // 应用新标志 - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags |= Qt::FramelessWindowHint; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags |= Qt::WindowStaysOnTopHint; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags |= Qt::WindowStaysOnBottomHint; - - setWindowFlags(flags); - - // 全屏处理 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - showFullScreen(); - } else if (isFullScreen()) { - showNormal(); - } - - // 大小调整 - if (!behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - setFixedSize(size()); - } - } -}; - -} // namespace desktop -```yaml - ---- - -## 设计原则与最佳实践 - -### 1. Flag = 能力集合(Capability Set) - -设计标志位时,应该将其视为"能力集合"而非"状态集合": - -```cpp -// ✅ 正确:描述能力 -enum class CapabilityFlag { - CanFullscreen = 1 << 0, // 支持全屏的能力 - CanResize = 1 << 1, // 支持调整大小的能力 -}; - -// ❌ 错误:描述状态 -enum class StateFlag { - IsFullscreen = 1 << 0, // 当前是否全屏 - IsResizing = 1 << 1, // 当前是否正在调整大小 -}; -```text - -### 2. 不返回单个枚举值 - -查询接口应该返回标志组合而非单个枚举值: - -```cpp -// ❌ 错误:返回单个枚举 -DesktopBehaviorFlag getCurrentBehavior() { - return DesktopBehaviorFlag::Fullscreen; -} - -// ✅ 正确:返回标志组合 -DesktopBehaviors getCurrentBehaviors() { - return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -} -```text - -### 3. 保持 ABI 可扩展 - -使用位偏移而非连续值,方便后续扩展: - -```cpp -// ✅ 正确:使用位移 -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - // 未来可以添加更多标志 - NewFeature = 1 << 12, -}; - -// ❌ 错误:使用连续值 -enum class DesktopBehaviorFlag { - Fullscreen = 1, - Frameless = 2, - StayOnTop = 3, - // 添加新标志容易冲突 -}; -```text - -### 4. 使用 enum class 提高类型安全 - -C++11 引入的 `enum class` 提供了更强的类型安全: - -```cpp -// ✅ 使用 enum class -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, - // ... -}; - -// ❌ 使用普通 enum -enum DesktopBehaviorFlag { - Fullscreen = 1 << 0, - // ... -}; -```text - -`enum class` 的优势: - -- 作用域枚举值,避免命名冲突 -- 隐式转换被禁用,提高类型安全 -- 更明确的前向声明 - -### 5. 提供便捷的测试方法 - -封装常用的测试逻辑: - -```cpp -// ✅ 封装测试方法 -class DesktopBehaviorsUtil { -public: - static bool isFullscreen(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Fullscreen); - } - - static bool isFrameless(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Frameless); - } - - static bool isAlwaysOnTop(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::StayOnTop); - } - - static bool isResizable(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::AllowResize); - } - - // 组合测试 - static bool isNormalWindow(const DesktopBehaviors& behaviors) { - return behaviors == DesktopBehaviorFlag::None; - } - - static bool isDialog(const DesktopBehaviors& behaviors) { - return behaviors.testFlag(DesktopBehaviorFlag::Modal) || - behaviors.testFlag(DesktopBehaviorFlag::Popup); - } -}; -```text - -### 6. 使用 constexpr 编译时计算 - -对于预定义的行为组合,使用 `constexpr` 确保编译时计算: - -```cpp -// ✅ 使用 constexpr -constexpr auto NormalBehavior = DesktopBehaviorFlag::None; -constexpr auto FullscreenBehavior = DesktopBehaviorFlag::Fullscreen; -constexpr auto FramelessBehavior = DesktopBehaviorFlag::Frameless; -constexpr auto AlwaysOnTopBehavior = - DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::Frameless; -```text - -### 7. 文档化标志位含义 - -为每个标志位提供清晰的文档注释: - -```cpp -/** - * @brief 桌面窗口行为标志 - * - * 这些标志位用于描述窗口的各种行为特性。 - * 多个标志位可以使用位运算符组合使用。 - */ -enum class DesktopBehaviorFlag { - None = 0, ///< 无特殊行为 - - /** - * @brief 全屏模式 - * - * 窗口占据整个屏幕,隐藏系统 UI(任务栏等)。 - * 与 Frameless 标志位配合使用时,将创建真正的无边框全屏窗口。 - */ - Fullscreen = 1 << 0, - - /** - * @brief 无边框窗口 - * - * 移除窗口的标题栏和边框。 - * 注意:无边框窗口需要自行实现窗口拖动功能。 - */ - Frameless = 1 << 1, - - /** - * @brief 置顶窗口 - * - * 窗口保持在其他窗口之上。 - * 注意:某些平台(如 Wayland)可能不支持此标志位。 - * - * @see Qt::WindowStaysOnTopHint - */ - StayOnTop = 1 << 2, - - // ... 更多标志位 -}; -```yaml - ---- - -## 常见陷阱与解决方案 - -### 陷阱 1:忘记 Q_DECLARE_OPERATORS_FOR_FLAGS - -```cpp -// ❌ 错误:忘记声明运算符 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -// 缺少 Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -// 编译错误:没有匹配的 operator| -```text - -**解决方案**:总是成对使用这两个宏 - -```cpp -// ✅ 正确 -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) -```text - -参考:[QFlags tutorial - Qt Wiki](https://wiki.qt.io/QFlags_tutorial) - -### 陷阱 2:enum class 隐式转换问题 - -```cpp -// ❌ 错误:enum class 不能隐式转换为整数 -enum class DesktopBehaviorFlag { - Fullscreen = 1 << 0, -}; - -DesktopBehaviorFlag flag = DesktopBehaviorFlag::Fullscreen; -int value = flag; // 编译错误 -```text - -**解决方案**:显式转换 - -```cpp -// ✅ 正确 -int value = static_cast(flag); -```text - -### 陷阱 3:位运算符优先级 - -```cpp -// ❌ 错误:位运算符优先级低于比较运算符 -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -if (b & DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None) { - // 这永远不会执行!因为 == 优先级高于 & -} - -// 实际解析为:if (b & (DesktopBehaviorFlag::Fullscreen == DesktopBehaviorFlag::None)) -// 即:if (b & false) -// 即:if (b & 0) -```text - -**解决方案**:使用括号 - -```cpp -// ✅ 正确 -if ((b & DesktopBehaviorFlag::Fullscreen) == DesktopBehaviorFlag::None) { - // 或者使用 testFlag() - if (!b.testFlag(DesktopBehaviorFlag::Fullscreen)) { -```text - -### 陷阱 4:移除标志位时忘记取反 - -```cpp -// ❌ 错误:忘记取反 -DesktopBehaviors b = DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; -b &= DesktopBehaviorFlag::Fullscreen; -// 结果:只剩下 Fullscreen,而不是移除 Fullscreen - -// 正确的做法是: -b &= ~DesktopBehaviorFlag::Fullscreen; -// 结果:只剩下 Frameless -```text - -### 陷阱 5:标志位冲突 - -```cpp -// ❌ 错误:StayOnTop 和 StayOnBottom 不应该同时存在 -DesktopBehaviors b = DesktopBehaviorFlag::StayOnTop | DesktopBehaviorFlag::StayOnBottom; -```text - -**解决方案**:在应用时进行冲突检测 - -```cpp -// ✅ 正确:添加冲突检测 -class DesktopBehaviorsUtil { -public: - static DesktopBehaviors resolveConflicts(DesktopBehaviors behaviors) { - // StayOnTop 和 StayOnBottom 冲突,优先保留 StayOnTop - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop) && - behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - - // Fullscreen 和 AllowResize 冲突 - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen) && - behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - behaviors &= ~DesktopBehaviorFlag::AllowResize; - } - - return behaviors; - } -}; -```text - -### 陷阱 6:跨平台兼容性 - -某些标志位在不同平台上的行为不一致: - -```cpp -// ⚠️ 警告:StayOnTop 在 Wayland 上可能不工作 -behaviors |= DesktopBehaviorFlag::StayOnTop; -```text - -**解决方案**:添加平台检测 - -```cpp -// ✅ 正确:平台检测 -#include - -class DesktopBehaviorsUtil { -public: - static DesktopBehaviors filterPlatformSupported(DesktopBehaviors behaviors) { -#if defined(Q_OS_WIN) - // Windows 完全支持 - return behaviors; -#elif defined(Q_OS_LINUX) - // Linux 需要区分 X11 和 Wayland - if (QGuiApplication::platformName() == "wayland") { - // Wayland 不支持某些标志位 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - return behaviors; -#else - // 其他平台 - return behaviors; -#endif - } -}; -```bash - -参考:[Wayland and Qt - Qt 6.11 文档](https://doc.qt.io/qt-6/wayland-and-qt.html) - ---- - -## 与其他方案的对比 - -### 方案对比表 - -| 方案 | 内存占用 | 组合性 | 类型安全 | 扩展性 | Qt 集成 | 推荐场景 | -|------|---------|-------|---------|--------|---------|---------| -| struct bool | 高(N 字节) | 差 | 中 | 差 | 需手动转换 | 简单场景,少量标志 | -| QFlags | 低(1-4 字节) | 优 | 优 | 优 | 原生支持 | Qt 项目,推荐使用 | -| std::bitset | 低(1+ 字节) | 中 | 中 | 中 | 需手动转换 | 非 Qt 项目,固定位数 | -| uint32_t | 低(4 字节) | 优 | 差 | 优 | 需手动转换 | 底层代码,性能关键 | - -### QFlags vs std::bitset - -```cpp -// QFlags -enum class Flag { A = 1 << 0, B = 1 << 1 }; -Q_DECLARE_FLAGS(Flags, Flag) -Q_DECLARE_OPERATORS_FOR_FLAGS(Flags) -Flags f = Flag::A | Flag::B; - -// std::bitset -std::bitset<2> f; -f[0] = true; // Flag A -f[1] = true; // Flag B -```text - -**QFlags 优势**: -- 类型安全(编译时检查) -- 符号名称(调试友好) -- Qt 元对象系统集成 -- 运算符更自然 - -**std::bitset 优势**: -- 动态大小(编译时确定) -- 更多操作(count, any, none, all) -- 标准库支持 - -### QFlags vs uint32_t - -```cpp -// QFlags -Flags f = Flag::A | Flag::B; -if (f.testFlag(Flag::A)) { } - -// uint32_t -uint32_t f = 0x03; -if (f & 0x01) { } -```yaml - -**QFlags 优势**: -- 类型安全 -- 自文档化(符号名称) -- 运算符重载 -- testFlag() 方法 - -**uint32_t 优势**: -- 跨语言兼容 -- 序列化简单 -- 底层控制 - ---- - -## 总结 - -### 核心要点 +最终选择 `QFlags` 作为行为描述的统一类型。QFlags 提供编译时类型安全(防止误将不相关枚举混入)、原生位运算符重载(`|`, `&`, `^`, `~`)、与 QVariant/QMetaObject 的无缝集成,以及 `testFlag()` 等便捷 API。使用 `1 << N` 位偏移赋值确保新标志位可以随时追加而不影响现有值。 -1. **类型安全**:`QFlags` 提供了比裸整数更安全的类型系统 -2. **组合能力**:位运算符使得行为组合变得简洁高效 -3. **Qt 集成**:与 `QVariant`、元对象系统、QML 无缝集成 -4. **可扩展性**:使用位偏移确保 ABI 可扩展 -5. **平台兼容**:需要注意跨平台差异,特别是 Wayland +行为标志位在概念上被定义为**能力集合(Capability Set)**而非状态集合,与 Strategy 系统配合描述"窗口应该具备什么行为",而非"窗口当前是什么状态"。 -### 参考资源 +## 关键决策 -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [QFlags tutorial - Qt Wiki](https://wiki.qt.io/QFlags_tutorial) -- [New in Qt 5.5: Q_ENUM and the C++ tricks behind it - Woboq](https://woboq.com/blog/q_enum.html) -- [C++11 standard conformant bitmasks using enum class - Stack Overflow](https://stackoverflow.com/questions/12059774/c11-standard-conformant-bitmasks-using-enum-class) -- [Typesafe Enum Class Bitmasks in C++ - StrikerX3.dev](https://www.strikerx3.dev/cpp/2019/02/27/typesafe-enum-class-bitmasks-in-cpp.html) +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 使用 `QFlags` + `enum class` (bit-shift 赋值) | 类型安全、Qt 集成、可扩展、位运算组合 | `struct { bool ... }`(不可组合、扩展性差) | +| `DesktopBehaviorFlag` 值: `Fullscreen=0x1, Frameless=0x2, StayOnBottom=0x4, AllowResize=0x8, AvoidSystemUI=0x10` | 精简为实际使用的 5 个标志,移除未实现的 StayOnTop/Transparent/ClickThrough/Modal/Popup/Tool/Splash | 原始草案包含 12+ 标志位(多数无对应实现) | +| 通过 `Q_DECLARE_FLAGS` / `Q_DECLARE_OPERATORS_FOR_FLAGS` 宏声明 | 使 `DesktopBehaviorFlag::X \| DesktopBehaviorFlag::Y` 在全局作用域合法 | 手写 `operator|` 重载(冗余且易遗漏) | +| Flag = Capability Set 语义 | 与 Strategy 系统的 `query()` / `action()` 分离对齐 | Flag = State 语义(状态应由 `query()` 运行时检测) | -### 下一步 +## 当前状态 -在下一篇文档中,我们将深入探讨 **Qt 窗口行为解析**,了解如何从 `QWidget` 的状态反推行为,以及跨平台行为的差异处理。 +已实现。标志枚举和 QFlags 声明位于 `desktop/ui/platform/IDesktopDisplaySizeStrategy.h`,作为 Strategy 接口的一部分对外暴露。 diff --git a/document/notes/02-Qt-Window-Behavior-Analysis.md b/document/notes/02-Qt-Window-Behavior-Analysis.md index 6099dfed8..f68bf1c16 100644 --- a/document/notes/02-Qt-Window-Behavior-Analysis.md +++ b/document/notes/02-Qt-Window-Behavior-Analysis.md @@ -1,985 +1,35 @@ --- title: Qt 窗口行为解析:QWidget 到 DesktopBehaviors -description: 1. 引言:为什么需要从 Qt 状态反推行为 +description: Qt WindowFlags 到 DesktopBehaviors 的映射关系及跨平台差异 --- -# Qt 窗口行为解析:QWidget 到 DesktopBehaviors +# Qt 窗口行为解析:QWidget 到 DesktopBehaviors -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [引言:为什么需要从 Qt 状态反推行为](#引言为什么需要从-qt-状态反推行为) -2. [行为与 Qt API 映射表](#行为与-qt-api-映射表) -3. [从 QWidget 查询行为的标准实现](#从-qwidget-查询行为的标准实现) -4. [跨平台行为差异分析](#跨平台行为差异分析) -5. [不可直接获取的行为推断](#不可直接获取的行为推断) -6. [平台特定的行为查询策略](#平台特定的行为查询策略) -7. [实践示例与最佳实践](#实践示例与最佳实践) +CFDesktop 定义了平台无关的 `DesktopBehaviors` 标志来描述窗口行为意图,但实际执行必须映射到 Qt 的 `QWidget` API。由于 Qt 的窗口状态分散在 `windowFlags()`、`isFullScreen()`、`minimumSize()/maximumSize()` 等多个 API 中,且部分行为(如 AvoidSystemUI)根本没有直接对应的 Qt API,需要通过多条件推断,因此需要一份明确的映射表作为所有平台策略实现的参考契约。 ---- - -## 引言:为什么需要从 Qt 状态反推行为 - -### 问题背景 - -在桌面应用程序开发中,我们经常面临以下场景: - -1. **日志记录需求**:需要记录窗口当前的行为状态用于调试 -2. **状态同步**:多个组件需要了解窗口的当前行为配置 -3. **动态决策**:根据当前窗口状态决定后续操作 -4. **策略融合**:多个策略模块需要基于当前状态进行组合判断 - -在 CFDesktop 项目中,我们定义了 `DesktopBehaviors` 作为抽象的行为描述层,而 Qt 的 `QWidget` 则是底层实现。如何从 `QWidget` 的当前状态准确反推出对应的 `DesktopBehaviors`,是一个需要深入理解 Qt 窗口系统的问题。 - -### Qt 窗口系统概述 - -根据 [Qt 6.11 QWidget 官方文档](https://doc.qt.io/qt-6/qwidget.html): - -> "Window flags are a combination of a type (e.g. Qt::Dialog) and zero or more hints to the window system (e.g. Qt::FramelessWindowHint)." - -Qt 的窗口标志(Window Flags)由两部分组成: - -1. **窗口类型(Window Type)**:定义窗口的基本性质,只能有一个 -2. **窗口提示(Window Hints)**:定制窗口外观和行为的可选标志,可以有多个 - -```cpp -Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; -```cpp - -### 反推行为的挑战 - -从 Qt 状态反推行为存在以下挑战: - -| 挑战 | 描述 | 示例 | -|------|------|------| -| **状态分散** | 同一行为可能由多个 API 共同决定 | `AllowResize` 需要检查 `minimumSize()` 和 `maximumSize()` | -| **平台差异** | 同一标志在不同平台表现不同 | `WindowStaysOnBottomHint` 在 X11 上不稳定 | -| **隐式行为** | 某些行为无法直接查询 | `AvoidSystemUI` 是隐式的,需要推断 | -| **状态动态变化** | 窗口状态可能在运行时改变 | `isFullScreen()` 状态可能与创建时的标志不同 | -| **标志组合冲突** | 某些标志组合会产生意外结果 | `FramelessWindowHint` + `WindowTitleHint` | - ---- - -## 行为与 Qt API 映射表 - -### 核心映射表 - -这是 `DesktopBehaviorFlag` 到 Qt API 的完整映射关系: - -| DesktopBehaviorFlag | Qt API / 状态 | 获取方式 | 平台兼容性 | -|---------------------|---------------|---------|-----------| -| **Fullscreen** | `isFullScreen()` | `widget->isFullScreen()` | 全平台 | -| **Frameless** | `Qt::FramelessWindowHint` | `widget->windowFlags() & Qt::FramelessWindowHint` | 全平台 | -| **StayOnTop** | `Qt::WindowStaysOnTopHint` | `widget->windowFlags() & Qt::WindowStaysOnTopHint` | 全平台(Wayland 限制) | -| **StayOnBottom** | `Qt::WindowStaysOnBottomHint` | `widget->windowFlags() & Qt::WindowStaysOnBottomHint` | 全平台(X11 限制) | -| **AllowResize** | `minimumSize()` / `maximumSize()` | 尺寸比较 | 全平台 | -| **AvoidSystemUI** | 多种标志组合 | 推断 | 平台相关 | - -### 详细 API 映射 - -#### 1. Fullscreen(全屏模式) - -```cpp -// 直接查询 API -bool isFullscreen = widget->isFullScreen(); -```text - -**注意事项**: -- `isFullScreen()` 返回窗口当前是否处于全屏状态 -- 与 `showFullScreen()` / `showNormal()` 相关 -- 可能与窗口标志的初始设置不同 - -**平台差异**: -- **Windows**:完全支持,可以隐藏任务栏 -- **X11**:支持,依赖窗口管理器 -- **Wayland**:受限,需要 compositor 支持 -- **macOS**:支持,有自己的全屏动画 - -#### 2. Frameless(无边框窗口) - -```cpp -// 通过窗口标志查询 -Qt::WindowFlags flags = widget->windowFlags(); -bool isFrameless = (flags & Qt::FramelessWindowHint) != 0; -```text - -**相关标志**: -- `Qt::FramelessWindowHint`:无边框窗口 -- `Qt::CustomizeWindowHint`:自定义窗口标题栏 -- `Qt::WindowTitleHint`:窗口有标题栏 -- `Qt::WindowSystemMenuHint`:窗口有系统菜单 - -根据 [Window Flags Example 文档](https://doc.qt.io/qt-6/qtwidgets-widgets-windowflags-example.html): - -> "A window flag is either a type or a hint. A type is used to specify various window-system properties for the widget." - -**平台差异**: -- **Windows**:完全支持,需要自己实现窗口拖动 -- **X11**:支持,但窗口管理器可能添加装饰 -- **Wayland**:支持,但需要客户端装饰 -- **macOS**:受限,系统可能强制添加某些元素 - -#### 3. StayOnTop(置顶窗口) - -```cpp -bool isStayOnTop = (widget->windowFlags() & Qt::WindowStaysOnTopHint) != 0; -```text - -**相关标志**: -- `Qt::WindowStaysOnTopHint`:保持在其他窗口之上 -- `Qt::WindowStaysOnBottomHint`:保持在其他窗口之下(与置顶互斥) - -**平台差异**: -- **Windows**:完全支持 -- **X11**:支持,但依赖窗口管理器 -- **Wayland**:**不支持**,这是 Wayland 的安全限制 -- **macOS**:支持,但等级系统不同 - -#### 4. StayOnBottom(置底窗口) - -```cpp -bool isStayOnBottom = (widget->windowFlags() & Qt::WindowStaysOnBottomHint) != 0; -```text - -**平台差异**: -- **Windows**:完全支持 -- **X11**:**不稳定**,很多窗口管理器不完全支持 -- **Wayland**:**不支持** -- **macOS**:支持有限 - -#### 5. AllowResize(允许调整大小) - -```cpp -// 通过尺寸约束查询 -QSize minSize = widget->minimumSize(); -QSize maxSize = widget->maximumSize(); -bool isResizable = (minSize.isEmpty() || minSize.width() == 0) && - (maxSize.isEmpty() || maxSize.width() == QWIDGETSIZE_MAX); -```text - -**更精确的判断**: - -```cpp -bool isResizable(QWidget* widget) { - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - // 如果最小和最大尺寸相等,则不可调整 - if (minSize.isValid() && maxSize.isValid() && minSize == maxSize) { - return false; - } - - // 检查宽度和高度是否都可以调整 - bool widthResizable = (minSize.width() == 0 || maxSize.width() == QWIDGETSIZE_MAX || - (maxSize.width() > minSize.width())); - bool heightResizable = (minSize.height() == 0 || maxSize.height() == QWIDGETSIZE_MAX || - (maxSize.height() > minSize.height())); - - return widthResizable && heightResizable; -} -```text - -**相关 API**: -- `setFixedSize()`:设置固定大小 -- `setMinimumSize()`:设置最小尺寸 -- `setMaximumSize()`:设置最大尺寸 -- `sizePolicy()`:尺寸策略 - -#### 6. AvoidSystemUI(避开系统 UI) - -这是一个**推断性**的行为标志,没有直接的 API 可以查询。 - -**推断方法**: - -```cpp -bool hasAvoidSystemUI(QWidget* widget) { - Qt::WindowFlags flags = widget->windowFlags(); - - // 方法 1:检查是否全屏 - if (widget->isFullScreen()) { - return true; - } - - // 方法 2:检查是否为 Splash/ToolTip/Popup 类型 - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - if (type == Qt::SplashScreen || type == Qt::ToolTip || type == Qt::Popup) { - return true; - } - - // 方法 3:检查是否为 Tool 类型 + Frameless - if (type == Qt::Tool && (flags & Qt::FramelessWindowHint)) { - return true; - } - - // 方法 4:检查是否有 X11BypassWindowManagerHint - if (flags & Qt::X11BypassWindowManagerHint) { - return true; - } - - return false; -} -```yaml - ---- - -## 从 QWidget 查询行为的标准实现 - -### queryFromWidget 函数实现 - -以下是完整的 `queryFromWidget` 函数实现,用于从 `QWidget` 查询所有行为标志: - -```cpp -#include -#include -#include "IDesktopDisplaySizeStrategy.h" - -namespace cf::desktop::platform_strategy { - -/** - * @brief 从 QWidget 查询当前的桌面行为 - * - * 该函数通过检查 QWidget 的各种状态和属性, - * 推断出当前窗口的 DesktopBehaviors 标志。 - * - * @param widget 要查询的窗口部件 - * @return DesktopBehaviors 查询到的行为标志组合 - */ -DesktopBehaviors queryFromWidget(QWidget* widget) { - if (!widget) { - return DesktopBehaviorFlag::None; - } - - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - Qt::WindowFlags flags = widget->windowFlags(); - - // 1. 检查全屏状态 - if (widget->isFullScreen()) { - behaviors |= DesktopBehaviorFlag::Fullscreen; - } - - // 2. 检查无边框状态 - if (flags & Qt::FramelessWindowHint) { - behaviors |= DesktopBehaviorFlag::Frameless; - } - - // 3. 检查置底状态 - // 注意:StayOnBottom 与 StayOnTop 互斥,优先检查 StayOnBottom - if (flags & Qt::WindowStaysOnBottomHint) { - behaviors |= DesktopBehaviorFlag::StayOnBottom; - } - - // 4. 检查是否允许调整大小 - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - bool canResizeWidth = (minSize.width() == 0 || maxSize.width() == QWIDGETSIZE_MAX || - (maxSize.width() > minSize.width())); - bool canResizeHeight = (minSize.height() == 0 || maxSize.height() == QWIDGETSIZE_MAX || - (maxSize.height() > minSize.height())); - - if (canResizeWidth && canResizeHeight) { - behaviors |= DesktopBehaviorFlag::AllowResize; - } - - // 5. 推断 AvoidSystemUI - behaviors |= inferAvoidSystemUI(widget); - - // 6. 平台特定的行为过滤 - behaviors = filterPlatformBehaviors(behaviors); - - return behaviors; -} - -/** - * @brief 推断是否避开系统 UI - * - * 避开系统 UI 是一个隐式行为,需要综合多个条件判断。 - */ -DesktopBehaviors inferAvoidSystemUI(QWidget* widget) { - DesktopBehaviors result = DesktopBehaviorFlag::None; - Qt::WindowFlags flags = widget->windowFlags(); - - // 获取窗口类型 - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - - // 检查全屏状态 - if (widget->isFullScreen()) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - - // 检查特殊窗口类型 - switch (type) { - case Qt::SplashScreen: - case Qt::ToolTip: - case Qt::Popup: - result |= DesktopBehaviorFlag::AvoidSystemUI; - break; - case Qt::Tool: - // Tool 类型 + Frameless 通常表示小工具 - if (flags & Qt::FramelessWindowHint) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - break; - default: - break; - } - - // 检查 X11 Bypass Window Manager 标志 - if (flags & Qt::X11BypassWindowManagerHint) { - result |= DesktopBehaviorFlag::AvoidSystemUI; - } - - return result; -} - -/** - * @brief 过滤平台不支持的行为标志 - * - * 不同平台对窗口行为标志的支持程度不同, - * 这个函数根据当前平台过滤掉不支持的标志。 - */ -DesktopBehaviors filterPlatformBehaviors(DesktopBehaviors behaviors) { -#ifdef Q_OS_WIN - // Windows 平台:完全支持所有标志 - return behaviors; -#elif defined(Q_OS_LINUX) - // Linux 平台:需要区分 X11 和 Wayland - QString platformName = QGuiApplication::platformName(); - - if (platformName == QLatin1String("wayland")) { - // Wayland 平台限制 - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; // Wayland 不支持 - - // StayOnTop 在 Wayland 上的支持有限 - // 某些 compositor 可能通过 layer-shell 协议支持 - // 但标准 Wayland 不支持 - } - // X11 平台:大部分支持,但 StayOnBottom 可能不稳定 - return behaviors; -#elif defined(Q_OS_MACOS) - // macOS 平台:大部分支持,但有特殊限制 - return behaviors; -#else - // 其他平台:保守处理 - return behaviors; -#endif -} - -/** - * @brief 获取行为的描述字符串(用于调试) - */ -QString behaviorDescription(const DesktopBehaviors& behaviors) { - QStringList descriptions; - - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - descriptions << QStringLiteral("Fullscreen"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) { - descriptions << QStringLiteral("Frameless"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - descriptions << QStringLiteral("StayOnBottom"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - descriptions << QStringLiteral("AllowResize"); - } - if (behaviors.testFlag(DesktopBehaviorFlag::AvoidSystemUI)) { - descriptions << QStringLiteral("AvoidSystemUI"); - } - - return descriptions.isEmpty() ? - QStringLiteral("None") : - descriptions.join(QStringLiteral(", ")); -} - -} // namespace cf::desktop::platform_strategy -```bash - ---- - -## 跨平台行为差异分析 - -### 平台支持矩阵 - -| 行为标志 | Windows | X11 | Wayland | macOS | EGLFS | Framebuffer | -|---------|---------|-----|---------|-------|-------|-------------| -| Fullscreen | 完全支持 | 支持 | 受限 | 支持 | 支持 | 支持 | -| Frameless | 完全支持 | 支持 | 需客户端装饰 | 受限 | 支持 | 支持 | -| StayOnTop | 完全支持 | 支持 | **不支持** | 支持 | 不支持 | 不支持 | -| StayOnBottom | 完全支持 | **不稳定** | **不支持** | 有限 | 不支持 | 不支持 | -| AllowResize | 完全支持 | 支持 | 支持 | 支持 | 支持 | 受限 | -| AvoidSystemUI | 完全支持 | 受限 | **严格限制** | 受限 | 支持 | 支持 | - -### 平台详细分析 - -#### Windows 平台 - -**特点**:Qt 在 Windows 上的原生平台,支持最全面 - -**完全支持的行为**: -- `Fullscreen`:真正的全屏,可以隐藏任务栏 -- `Frameless`:完全无边框,可以创建自定义标题栏 -- `StayOnTop`:使用 `HWND_TOPMOST` 实现 -- `StayOnBottom`:使用 `HWND_BOTTOM` 实现 - -**特殊标志**: -- `Qt::MSWindowsFixedSizeDialogHint`:创建固定大小的对话框 -- `Qt::WindowSystemMenuHint`:添加系统菜单 - -**代码示例**: - -```cpp -// Windows 平台最佳实践 -#ifdef Q_OS_WIN - // 创建无边框置顶窗口 - Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint; - widget->setWindowFlags(flags); - - // Windows 特有的无边框处理 - // 需要处理 WM_NCHITTEST 消息实现窗口拖动 -#endif -```bash - -**参考**:[MSDN Window Styles](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles) - -#### X11 平台 - -**特点**:依赖窗口管理器(Window Manager),行为多样 - -**支持情况**: -- `Fullscreen`:支持,通过 `_NET_WM_STATE_FULLSCREEN` EWMH 提示 -- `Frameless`:支持,通过 `MOTIF_WM_HINTS` 属性 -- `StayOnTop`:支持,通过 `_NET_WM_STATE_ABOVE` -- `StayOnBottom`:**不稳定**,通过 `_NET_WM_STATE_BELOW`,但不是所有 WM 都支持 - -**窗口管理器差异**: - -| 窗口管理器 | StayOnBottom | Frameless | 备注 | -|-----------|-------------|-----------|------| -| KWin | 支持 | 支持 | KDE 默认 WM | -| Mutter | 有限支持 | 支持 | GNOME 默认 WM | -| Openbox | 支持 | 支持 | 轻量级 WM | -| i3/sway | 支持 | 支持 | 平铺式 WM | - -**X11 特有标志**: -- `Qt::X11BypassWindowManagerHint`:绕过窗口管理器 - -```cpp -// X11 平台注意事项 -#ifdef Q_OS_LINUX - if (QGuiApplication::platformName() == QLatin1String("xcb")) { - // X11 特定处理 - - // 检查窗口管理器支持 - // StayOnBottom 在某些 WM 上可能不工作 - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - qWarning() << "StayOnBottom may not work properly on this window manager"; - } - } -#endif -```text - -#### Wayland 平台 - -**特点**:安全优先的协议,限制客户端的窗口控制能力 - -**核心限制**(根据 [Wayland and Qt 文档](https://doc.qt.io/qt-6/wayland-and-qt.html)): - -> "The Wayland protocol is designed with security and isolation in mind, and is strict/conservative about what information and functionality is available to clients." - -**不支持的行为**: -- `StayOnTop`:**完全不支持**(标准协议) -- `StayOnBottom`:**完全不支持** -- 窗口位置查询:客户端无法查询自己的窗口位置 -- 全局输入捕获:不支持 - -**受限的行为**: -- `Fullscreen`:需要 compositor 支持,通过 `xdg-shell` 协议 -- `Frameless`:需要客户端装饰(CSD),不支持服务端装饰(SSD) - -**Wayland 特殊考虑**: - -```cpp -// Wayland 平台特殊处理 -#ifdef Q_OS_LINUX - if (QGuiApplication::platformName() == QLatin1String("wayland")) { - // 移除不支持的标志 - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - - // Wayland 上的 Frameless 需要 CSD - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) { - // 需要实现客户端装饰 - } - } -#endif -```yaml - -**Wayland 协议扩展**: -某些 compositor 提供扩展协议: -- `xdg-shell`:标准的 shell 协议 -- `layer-shell`:用于面板/覆盖层的协议 -- `xdg-decoration`:装饰协商协议 - -#### EGLFS 平台 - -**特点**:嵌入式 Linux 平台,无窗口管理器 - -**支持情况**: -- `Fullscreen`:完全支持(默认行为) -- `Frameless`:支持(默认无装饰) -- `StayOnTop/Bottom`:**不支持**(无窗口管理器) -- `AllowResize`:支持有限 - -**适用场景**: -- 嵌入式设备 -- 信息亭模式 -- 数字标牌 - -#### Framebuffer 平台 - -**特点**:直接写入帧缓冲,最低级别的平台 - -**支持情况**: -- `Fullscreen`:完全支持(唯一模式) -- `Frameless`:完全支持(无窗口系统) -- `StayOnTop/Bottom`:**不支持**(无窗口堆栈) -- `AllowResize`:不支持(单一表面) - -**适用场景**: -- 极简嵌入式系统 -- 启动画面 -- 硬件直接访问 - ---- - -## 不可直接获取的行为推断 - -### 问题概述 - -某些 `DesktopBehaviorFlag` 不能直接从 Qt API 获取,需要通过以下方法推断: - -1. **组合条件推断**:多个条件综合判断 -2. **状态变化监听**:通过事件推断意图 -3. **平台特定 API**:使用原生平台 API -4. **启发式推断**:基于常见模式猜测 - -### AvoidSystemUI 推断详解 - -`AvoidSystemUI` 是最复杂的推断性标志,表示窗口希望避开系统 UI 元素(任务栏、Dock 等)。 - -#### 推断条件 - -```cpp -bool inferAvoidSystemUI(QWidget* widget) { - // 条件 1:全屏模式 - if (widget->isFullScreen()) { - return true; - } - - Qt::WindowFlags flags = widget->windowFlags(); - Qt::WindowType type = static_cast(flags & Qt::WindowType_Mask); - - // 条件 2:特殊窗口类型 - if (type == Qt::SplashScreen || // 启动画面 - type == Qt::ToolTip || // 工具提示 - type == Qt::Popup) { // 弹出菜单 - return true; - } - - // 条件 3:Tool + Frameless(小工具模式) - if (type == Qt::Tool && (flags & Qt::FramelessWindowHint)) { - return true; - } - - // 条件 4:X11 Bypass Window Manager - if (flags & Qt::X11BypassWindowManagerHint) { - return true; - } - - // 条件 5:检查位置(如果窗口位于屏幕边缘) - QRect screenGeometry = widget->screen()->geometry(); - QRect windowGeometry = widget->geometry(); - - // 窗口紧贴屏幕边缘,可能是面板/工具栏 - const int edgeThreshold = 10; - if (windowGeometry.left() <= screenGeometry.left() + edgeThreshold || - windowGeometry.top() <= screenGeometry.top() + edgeThreshold || - windowGeometry.right() >= screenGeometry.right() - edgeThreshold || - windowGeometry.bottom() >= screenGeometry.bottom() - edgeThreshold) { - return true; - } - - return false; -} -```text - -### AllowResize 推断详解 - -`AllowResize` 需要检查多个尺寸相关的属性: - -```cpp -bool inferAllowResize(QWidget* widget) { - // 方法 1:检查最小/最大尺寸 - QSize minSize = widget->minimumSize(); - QSize maxSize = widget->maximumSize(); - - // 如果设置了固定大小,则不允许调整 - if (minSize.isValid() && maxSize.isValid() && minSize == maxSize) { - return false; - } - - // 方法 2:检查 sizePolicy - QSizePolicy policy = widget->sizePolicy(); - if (policy.horizontalPolicy() == QSizePolicy::Fixed && - policy.verticalPolicy() == QSizePolicy::Fixed) { - return false; - } - - // 方法 3:检查是否有固定大小的设置 - // 通过 Qt::MSWindowsFixedSizeDialogHint 等标志 - Qt::WindowFlags flags = widget->windowFlags(); - if (flags & Qt::MSWindowsFixedSizeDialogHint) { - return false; - } - - // 方法 4:检查全屏状态(全屏时不可调整) - if (widget->isFullScreen()) { - return false; - } - - // 默认允许调整 - return true; -} -```text - -### 状态变化监听 - -对于某些行为,需要监听状态变化来推断: - -```cpp -class BehaviorTracker : public QObject { - Q_OBJECT -public: - explicit BehaviorTracker(QWidget* parent) - : QObject(parent), m_widget(parent) { - // 监听窗口标志变化 - connect(parent, &QWidget::windowTitleChanged, - this, &BehaviorTracker::onWindowFlagsChanged); - } - - DesktopBehaviors currentBehaviors() const { - return m_behaviors; - } - -signals: - void behaviorsChanged(DesktopBehaviors behaviors); - -private slots: - void onWindowFlagsChanged(const QString&) { - DesktopBehaviors newBehaviors = queryFromWidget(m_widget); - if (newBehaviors != m_behaviors) { - m_behaviors = newBehaviors; - emit behaviorsChanged(m_behaviors); - } - } - -private: - QWidget* m_widget; - DesktopBehaviors m_behaviors = DesktopBehaviorFlag::None; -}; -```yaml - ---- - -## 平台特定的行为查询策略 - -### 策略模式应用 - -在 CFDesktop 项目中,使用策略模式处理平台差异: - -```cpp -// 平台检测 -QString platformName() { - return QGuiApplication::platformName(); -} - -// 创建平台特定的策略 -std::unique_ptr createPlatformStrategy() { - QString platform = platformName(); - -#ifdef Q_OS_WIN - return std::make_unique(); -#elif defined(Q_OS_LINUX) - if (platform == QLatin1String("wayland")) { - return std::make_unique(); - } else if (platform == QLatin1String("xcb")) { - return std::make_unique(); - } else if (platform.startsWith(QLatin1String("eglfs"))) { - return std::make_unique(); - } -#elif defined(Q_OS_MACOS) - return std::make_unique(); -#endif - return std::make_unique(); -} -```text - -### 平台特定实现 - -#### Windows 策略 - -```cpp -class WindowsDisplaySizePolicy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - // Windows 平台完全支持所有标志 - // 可以直接查询 - - return behaviors; - } - - bool action(QWidget* widget) override { - // Windows 特定的行为应用逻辑 - return true; - } -}; -```text - -#### Wayland 策略 - -```cpp -class WaylandDisplaySizePolicy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - // Wayland 平台限制 - // 自动过滤不支持的标志 - - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - - return behaviors; - } - - bool action(QWidget* widget) override { - // Wayland 特定的行为应用逻辑 - // 需要与 compositor 协商 - - return true; - } -}; -```yaml - ---- - -## 实践示例与最佳实践 - -### 完整示例:行为查询工具类 - -```cpp -/** - * @file DesktopBehaviorQuery.h - * @brief 桌面窗口行为查询工具 - */ - -#pragma once -#include -#include -#include "IDesktopDisplaySizeStrategy.h" - -namespace cf::desktop::platform_strategy { - -/** - * @brief 行为查询结果 - */ -struct BehaviorQueryResult { - DesktopBehaviors behaviors; // 查询到的行为 - QString platformName; // 平台名称 - QStringList warnings; // 警告信息 - QStringList unsupported; // 不支持的标志 - - bool isValid() const { - return behaviors != DesktopBehaviorFlag::None || !unsupported.isEmpty(); - } - - QString toString() const { - return QStringLiteral("Platform: %1, Behaviors: %2") - .arg(platformName) - .arg(behaviorDescription(behaviors)); - } -}; - -/** - * @brief 桌面行为查询工具类 - * - * 提供从 QWidget 查询行为的完整功能, - * 包括平台检测、行为推断、警告提示等。 - */ -class DesktopBehaviorQuery { -public: - /** - * @brief 从 widget 查询行为 - * - * @param widget 要查询的窗口部件 - * @return BehaviorQueryResult 查询结果,包含行为、平台、警告等信息 - */ - static BehaviorQueryResult query(QWidget* widget) { - BehaviorQueryResult result; - - if (!widget) { - result.warnings << QStringLiteral("Widget is null"); - return result; - } - - // 1. 获取平台信息 - result.platformName = QGuiApplication::platformName(); - - // 2. 查询基础行为 - result.behaviors = queryFromWidget(widget); - - // 3. 检查平台支持 - checkPlatformSupport(result); - - // 4. 检查行为冲突 - checkBehaviorConflicts(result); - - return result; - } - -private: - /** - * @brief 检查平台对行为的支持情况 - */ - static void checkPlatformSupport(BehaviorQueryResult& result) { - QString platform = result.platformName; - -#ifdef Q_OS_LINUX - if (platform == QLatin1String("wayland")) { - // Wayland 不支持的标志 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) { - result.unsupported << QStringLiteral("StayOnTop"); - result.warnings << QStringLiteral("StayOnTop is not supported on Wayland"); - } - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.unsupported << QStringLiteral("StayOnBottom"); - result.warnings << QStringLiteral("StayOnBottom is not supported on Wayland"); - } - } else if (platform == QLatin1String("xcb")) { - // X11 可能不稳定的标志 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.warnings << QStringLiteral("StayOnBottom may be unstable on X11"); - } - } -#endif - } - - /** - * @brief 检查行为之间的冲突 - */ - static void checkBehaviorConflicts(BehaviorQueryResult& result) { - // Fullscreen 和 AllowResize 冲突 - if (result.behaviors.testFlag(DesktopBehaviorFlag::Fullscreen) && - result.behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) { - result.warnings << QStringLiteral("Fullscreen conflicts with AllowResize"); - } - - // StayOnTop 和 StayOnBottom 冲突 - if (result.behaviors.testFlag(DesktopBehaviorFlag::StayOnTop) && - result.behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) { - result.warnings << QStringLiteral("StayOnTop conflicts with StayOnBottom"); - } - } -}; - -} // namespace cf::desktop::platform_strategy -```text - -### 使用示例 - -```cpp -#include "DesktopBehaviorQuery.h" - -void logWidgetBehaviors(QWidget* widget) { - // 查询行为 - auto result = DesktopBehaviorQuery::query(widget); - - // 输出结果 - qDebug() << "Platform:" << result.platformName; - qDebug() << "Behaviors:" << behaviorDescription(result.behaviors); - - // 输出警告 - if (!result.warnings.isEmpty()) { - qWarning() << "Warnings:" << result.warnings; - } - - // 输出不支持的标志 - if (!result.unsupported.isEmpty()) { - qDebug() << "Unsupported on this platform:" << result.unsupported; - } -} -```text - -### 最佳实践总结 - -1. **始终进行平台检测**:在应用任何行为前,检测当前平台 -2. **优雅降级**:对于不支持的标志,提供替代方案或警告 -3. **监听状态变化**:窗口行为可能在运行时改变 -4. **文档化平台限制**:清晰记录每个平台的限制 -5. **提供测试工具**:帮助开发者理解不同平台的行为差异 - -### 调试技巧 - -```cpp -/** - * @brief 调试辅助函数:打印窗口的详细信息 - */ -void debugWindowInfo(QWidget* widget) { - qDebug() << "=== Window Debug Info ==="; - qDebug() << "Platform:" << QGuiApplication::platformName(); - qDebug() << "Window flags:" << widget->windowFlags(); - qDebug() << "Is fullscreen:" << widget->isFullScreen(); - qDebug() << "Is visible:" << widget->isVisible(); - qDebug() << "Is top level:" << widget->isWindow(); - qDebug() << "Geometry:" << widget->geometry(); - qDebug() << "Minimum size:" << widget->minimumSize(); - qDebug() << "Maximum size:" << widget->maximumSize(); - qDebug() << "Size policy:" << widget->sizePolicy(); - qDebug() << "========================"; -} -```yaml - ---- - -## 总结 - -### 核心要点 - -1. **行为反推是必要的**:在日志、同步、决策等场景中需要从 Qt 状态反推行为 -2. **API 映射是复杂的**:同一行为可能涉及多个 Qt API -3. **平台差异显著**:不同平台对窗口行为的支持程度不同 -4. **某些行为需要推断**:如 `AvoidSystemUI`、`AllowResize` 不能直接查询 -5. **策略模式是最佳实践**:使用策略模式处理平台差异 - -### 参考资源 +## 关键决策 -#### Qt 官方文档 +**核心映射表** -- `DesktopBehaviorFlag` 到 Qt API 的查询方式: -- [QWidget Class | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qwidget.html) -- [Window Flags Example | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qtwidgets-widgets-windowflags-example.html) -- [Wayland and Qt | Qt 6.11](https://doc.qt.io/qt-6/wayland-and-qt.html) -- [Qt Namespace | Qt Core | Qt 6.11.0](https://doc.qt.io/qt-6/qt.html) -- [QWindow Class | Qt GUI | Qt 6.11.0](https://doc.qt.io/qt-6/qwindow.html) -- [Window and Dialog Widgets](https://doc.qt.io/qt-6/application-windows.html) +| DesktopBehaviorFlag | Qt API / 状态 | 获取方式 | +|---------------------|---------------|---------| +| **Fullscreen** | `isFullScreen()` | `widget->isFullScreen()` | +| **Frameless** | `Qt::FramelessWindowHint` | `widget->windowFlags() & Qt::FramelessWindowHint` | +| **StayOnBottom** | `Qt::WindowStaysOnBottomHint` | `widget->windowFlags() & Qt::WindowStaysOnBottomHint` | +| **AllowResize** | `minimumSize()` / `maximumSize()` | min/max 尺寸比较推断 | +| **AvoidSystemUI** | 多条件推断 | 全屏 / SplashScreen+ToolTip+Popup 类型 / Tool+Frameless / X11BypassWM | -#### 相关文档 +**跨平台差异** -- 关键限制: -- [EGLFS - Qt for Linux](https://doc.qt.io/qt-6/embedded-linux.html) -- [LinuxFB Platform Plugin](https://doc.qt.io/qt-6/linuxfb.html) -- [X11 Platform Notes](https://doc.qt.io/qt-6/x11-platform.html) +| 行为 | Windows | X11 | Wayland | EGLFS | +|------|---------|-----|---------|-------| +| Fullscreen | 完全支持 | 支持 | 受限 | 支持 | +| Frameless | 完全支持 | 支持 | 需 CSD | 支持 | +| StayOnBottom | 完全支持 | 不稳定 | 不支持 | 不支持 | +| AvoidSystemUI | 完全支持 | 受限 | 严格限制 | 支持 | -### 下一步 +## 当前状态 -在下一篇文档中,我们将深入探讨 **桌面策略系统设计**,了解如何使用 Strategy 模式来管理不同平台的行为差异。 +映射逻辑通过各平台的 `IDesktopDisplaySizeStrategy` 实现落地:`desktop/ui/platform/windows/windows_display_size_policy.{h,cpp}`(Windows)和 `desktop/ui/platform/linux_wsl/linux_wsl_display_size_policy.{h,cpp}`(WSL/X11)。 diff --git a/document/notes/03-Desktop-Strategy-Pattern-Design.md b/document/notes/03-Desktop-Strategy-Pattern-Design.md index 8fae82169..71787826b 100644 --- a/document/notes/03-Desktop-Strategy-Pattern-Design.md +++ b/document/notes/03-Desktop-Strategy-Pattern-Design.md @@ -1,2141 +1,28 @@ --- title: 桌面策略系统设计(Strategy Pattern 实战) -description: 1. 为什么使用 Strategy 模式 +description: 使用 Strategy 模式 + CQRS 原则管理跨平台窗口行为 --- -# 桌面策略系统设计(Strategy Pattern 实战) +# 桌面策略系统设计(Strategy Pattern 实战) -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [为什么使用 Strategy 模式](#为什么使用-strategy-模式) - - [1.1 跨平台差异处理](#11-跨平台差异处理) - - [1.2 行为解耦与单一职责](#12-行为解耦与单一职责) - - [1.3 插件化架构支持](#13-插件化架构支持) - - [1.4 测试友好性](#14-测试友好性) -2. [Strategy Pattern 理论基础](#strategy-pattern-理论基础) - - [2.1 设计模式定义](#21-设计模式定义) - - [2.2 UML 结构图](#22-uml-结构图) - - [2.3 C++ 实现要点](#23-c-实现要点) -3. [接口设计:IDesktopDisplaySizeStrategy](#接口设计idesktopdisplaysizestrategy) - - [3.1 基础接口定义](#31-基础接口定义) - - [3.2 策略类型枚举](#32-策略类型枚举) - - [3.3 ABI 友好设计](#33-abi-友好设计) -4. [Action vs Query:核心思想对比](#action-vs-query核心思想对比) - - [4.1 CQRS 原则](#41-cqrs-原则) - - [4.2 Action 方法设计](#42-action-方法设计) - - [4.3 Query 方法设计](#43-query-方法设计) - - [4.4 分离的好处](#44-分离的好处) -5. [示例策略实现](#示例策略实现) - - [5.1 FullscreenStrategy](#51-fullscreenstrategy) - - [5.2 FramelessStrategy](#52-framelessstrategy) - - [5.3 WSL 平台策略](#53-wsl-平台策略) -6. [多策略组合实现](#多策略组合实现) - - [6.1 策略组合模式](#61-策略组合模式) - - [6.2 CompositeStrategy](#62-compositestrategy) - - [6.3 策略链模式](#63-策略链模式) -7. [冲突检测机制](#冲突检测机制) - - [7.1 行为冲突定义](#71-行为冲突定义) - - [7.2 冲突检测接口](#72-冲突检测接口) - - [7.3 冲突解决策略](#73-冲突解决策略) -8. [工厂模式集成](#工厂模式集成) - - [8.1 PlatformFactory 设计](#81-platformfactory-设计) - - [8.2 平台特定工厂](#82-平台特定工厂) - - [8.3 插件化工厂](#83-插件化工厂) -9. [最佳实践与设计原则](#最佳实践与设计原则) +窗口行为在不同操作系统上的实现截然不同(Windows 用 `WS_EX_TOPMOST`、X11 用 EWMH `_NET_WM_STATE_*`、Wayland 通过 xdg-shell 协议且禁止客户端控制 z-order)。如果直接在业务代码中堆砌 `#ifdef` 条件分支,会导致代码无法维护、无法测试、且每新增一个平台就需要修改所有分支。 ---- - -## 为什么使用 Strategy 模式 - -### 1.1 跨平台差异处理 - -在桌面应用开发中,不同操作系统对窗口行为有截然不同的实现方式: - -| 平台特性 | Windows | macOS | Linux (X11) | Linux (Wayland) | WSL | -|---------|---------|-------|------------|----------------|-----| -| 全屏实现 | `WM_SYSCOMMAND` | `NSFullScreenMode` | `_NET_WM_STATE_FULLSCREEN` | xdg-shell 全屏 | 代理实现 | -| 无边框窗口 | `WS_POPUP` | `NSWindowStyleMaskBorderless` | `_NET_WM_WINDOW_TYPE` | xdg-shell popup | 限制支持 | -| 窗口置顶 | `WS_EX_TOPMOST` | `NSWindowLevel` | `_NET_WM_STATE_ABOVE` | 不支持 | 不支持 | -| 窗口层级 | Z-Order | Window Level | EWMH 层级 | Wayland 协议 | 限制支持 | - -这种差异使得单一实现难以应对所有场景,而 Strategy 模式提供了一种优雅的解决方案: - -```cpp -// ❌ 不使用 Strategy:条件分支地狱 -void applyWindowFlags(QWidget* widget) { - #ifdef Q_OS_WIN - // Windows 特定代码 - #elif defined(Q_OS_MACOS) - // macOS 特定代码 - #elif defined(Q_OS_LINUX) - if (QGuiApplication::platformName() == "xcb") { - // X11 特定代码 - } else if (QGuiApplication::platformName() == "wayland") { - // Wayland 特定代码 - } - #endif - // 代码维护困难,难以扩展 -} - -// ✅ 使用 Strategy:平台无关接口 -void applyWindowFlags(QWidget* widget, IDesktopDisplaySizeStrategy* strategy) { - strategy->action(widget); // 平台特定实现被封装 -} -```text - -### 1.2 行为解耦与单一职责 - -Strategy 模式遵循**单一职责原则**(Single Responsibility Principle),将窗口行为的具体实现从主业务逻辑中分离: - -```cpp -// 职责分离示例 -class CFDesktop : public QWidget { - // CFDesktop 专注于: - // 1. 管理桌面组件(PanelManager、ShellLayer) - // 2. 提供代理访问(CFDesktopProxy) - // 3. 协调组件生命周期 - - // 不负责: - // ✗ 平台特定的窗口行为实现 - // ✗ 不同显示模式的具体逻辑 -}; - -// 策略专注于: -// 1. 实现特定平台的窗口行为 -// 2. 提供行为查询接口 -// 3. 管理平台相关的状态 -```text - -这种分离带来的好处: - -1. **代码可读性**:每个策略类职责明确,易于理解 -2. **可维护性**:修改平台特定逻辑不影响其他代码 -3. **可测试性**:可以独立测试每个策略 - -### 1.3 插件化架构支持 - -Strategy 模式为插件化架构提供了天然支持。通过工厂模式 + 策略模式,可以实现运行时动态加载平台实现: - -```cpp -// 插件化架构示例 -namespace cf::desktop::platform_strategy { - -// 平台工厂 API -struct PlatformFactoryAPI { - using CreateFunc = IDesktopPropertyStrategy*(StrategyType); - using ReleaseFunc = void(IDesktopPropertyStrategy*); - - CreateFunc creator_func; - ReleaseFunc release_func; -}; - -// 本地实现 -PlatformFactoryAPI native() noexcept; - -// TODO: 从 DLL 加载 -// PlatformFactoryAPI* remote() noexcept; - -} // namespace -```text - -这支持以下场景: - -1. **运行时平台检测**:根据运行环境自动选择策略 -2. **动态插件加载**:从共享库加载平台实现 -3. **A/B 测试**:可以同时测试不同的策略实现 - -### 1.4 测试友好性 - -Strategy 模式使得单元测试变得简单: - -```cpp -// 测试用例示例 -class MockDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Mock Strategy"; - } - - bool action(QWidget* widget) override { - action_called = true; - last_widget = widget; - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; - } - - // 测试辅助 - bool action_called = false; - QWidget* last_widget = nullptr; -}; - -// 使用 mock 进行测试 -TEST(DesktopTest, ApplyStrategy) { - MockDisplaySizeStrategy mock_strategy; - CFDesktop desktop; - - // 应用策略 - mock_strategy.action(&desktop); - - // 验证 - EXPECT_TRUE(mock_strategy.action_called); - EXPECT_EQ(mock_strategy.last_widget, &desktop); -} -```yaml - -参考资料: -- [Strategy in C++ / Design Patterns - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy/cpp/example) -- [The Strategy Pattern – MC++ BLOG - Modernes C++](https://www.modernescpp.com/index.php/the-strategy-pattern/) -- [Design Patterns: Elements of Reusable Object-Oriented Software - GoF](https://en.wikipedia.org/wiki/Design_Patterns) - ---- - -## Strategy Pattern 理论基础 - -### 2.1 设计模式定义 - -根据 GoF(Gang of Four)的经典定义: - -> **Strategy Pattern**:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。 - -在桌面行为管理场景中: - -- **算法**:窗口行为的具体实现(全屏、无边框、置顶等) -- **客户**:CFDesktop 或其他需要应用窗口行为的组件 -- **变化**:不同平台、不同配置下的行为差异 - -### 2.2 UML 结构图 - -```text -┌─────────────────────────────────────────────────────────────────────┐ -│ Context (CFDesktop) │ -│ │ -│ - strategy: IDesktopDisplaySizeStrategy │ -│ + setStrategy(strategy: IDesktopDisplaySizeStrategy) │ -│ + applyBehavior() │ -└─────────────────────────────┬───────────────────────────────────────┘ - │ - │ uses - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ <> IDesktopDisplaySizeStrategy │ -│ │ -│ + action(widget: QWidget*): bool │ -│ + query(): DesktopBehaviors │ -│ + name(): const char* │ -└─────────────────────────────┬───────────────────────────────────────┘ - │ - │ implements - ┌───────────────────┼───────────────────┐ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│FullscreenStrategy│ │FramelessStrategy│ │ WSLDisplaySize │ -│ │ │ │ │ Strategy │ -│+ action() │ │+ action() │ │+ action() │ -│+ query() │ │+ query() │ │+ query() │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -```text - -### 2.3 C++ 实现要点 - -#### 虚析构函数 - -策略接口必须声明虚析构函数以确保正确的资源清理: - -```cpp -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - virtual ~IDesktopDisplaySizeStrategy() = default; - // ... -}; -```text - -#### 智能指针支持 - -使用智能指针管理策略生命周期: - -```cpp -class PlatformFactory { -public: - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - // 使用 unique_ptr,自定义删除器 - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t); - - // 使用 shared_ptr,支持共享所有权 - std::shared_ptr - factorize_shared(const IDesktopPropertyStrategy::StrategyType t); -}; -```text - -#### WeakPtr 集成 - -策略支持 WeakPtr,避免循环引用: - -```cpp -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - WeakPtr GetOne() { - return weak_factory_ptr_.GetWeakPtr(); - } - -private: - WeakPtrFactory weak_factory_ptr_; -}; -```yaml - -参考资料: -- [Strategy Design Pattern - GeeksforGeeks](https://www.geeksforgeeks.org/system-design/strategy-pattern-set-1/) -- [C++ Core Guidelines: C.129 - When designing a class hierarchy, distinguish between implementation inheritance and interface inheritance](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-interface) - ---- - -## 接口设计:IDesktopDisplaySizeStrategy - -### 3.1 基础接口定义 - -```cpp -/** - * @file IDesktopPropertyStrategy.h - * @brief 桌面属性策略基础接口 - * - * 所有桌面策略的抽象基类,定义了策略的基本契约。 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 桌面属性策略基类 - * - * @details 提供所有策略共享的类型标识和接口契约 - */ -class IDesktopPropertyStrategy { -public: - /** - * @brief 策略类型枚举 - * - * 用于策略工厂的类型标识,确保类型安全的策略创建 - */ - enum class StrategyType { - Unavailable, ///< 无效/不可用策略 - DisplaySizePolicy, ///< 显示尺寸策略(全屏、无边框等) - Extensions, ///< 扩展策略(输入法、系统托盘等) - }; - - /** - * @brief 构造函数 - * @param t 策略类型 - */ - IDesktopPropertyStrategy(const StrategyType t); - - /** - * @brief 虚析构函数 - * - * 确保通过基类指针删除派生类对象时正确调用派生类析构函数 - */ - virtual ~IDesktopPropertyStrategy() = default; - - /** - * @brief 获取策略名称 - * - * @return 策略的名称字符串,用于调试和日志 - * - * @note 必须由派生类实现,返回有效的字符串字面量 - * @note 使用 noexcept 确保不会抛出异常 - */ - virtual const char* name() const noexcept = 0; - -protected: - const StrategyType type_; ///< 策略类型,运行时类型标识 -}; - -} // namespace cf::desktop::platform_strategy -```text - -### 3.2 策略类型枚举 - -策略类型枚举提供了类型安全的策略创建机制: - -```cpp -/** - * @brief 策略类型到字符串的转换(用于日志) - */ -inline const char* strategyTypeToString(IDesktopPropertyStrategy::StrategyType type) { - switch (type) { - case IDesktopPropertyStrategy::StrategyType::Unavailable: - return "Unavailable"; - case IDesktopPropertyStrategy::StrategyType::DisplaySizePolicy: - return "DisplaySizePolicy"; - case IDesktopPropertyStrategy::StrategyType::Extensions: - return "Extensions"; - default: - return "Unknown"; - } -} -```text - -### 3.3 ABI 友好设计 - -为了确保 ABI(Application Binary Interface)兼容性,接口设计遵循以下原则: - -#### 1. 避免虚模板方法 - -```cpp -// ❌ 不推荐:模板虚方法破坏 ABI -class IDesktopPropertyStrategy { -public: - template - virtual T get() = 0; // 编译错误或 ABI 问题 -}; - -// ✅ 推荐:使用显式类型方法 -class IDesktopPropertyStrategy { -public: - virtual const char* name() const noexcept = 0; - virtual StrategyType type() const noexcept { return type_; } -}; -```text - -#### 2. 使用 Pimpl 模式隐藏实现 - -```cpp -// 头文件:稳定 ABI -class IDesktopPropertyStrategy { -public: - // 公共接口... -private: - class Impl; - std::unique_ptr impl_; // 实现细节 -}; - -// 实现文件:可变实现 -class IDesktopPropertyStrategy::Impl { - // 实现细节可以随意修改而不影响 ABI -}; -```text - -#### 3. 自定义删除器 - -```cpp -// 使用自定义删除器确保跨 DLL 边界的正确释放 -class PlatformFactory { -public: - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t) { - // 创建策略并使用工厂的删除器 - return { createImpl(t), [](IDesktopPropertyStrategy* p) { - releaseImpl(p); // 使用工厂的释放函数 - }}; - } -}; -```text - -### 3.4 显示策略接口 - -```cpp -/** - * @file IDesktopDisplaySizeStrategy.h - * @brief 桌面显示尺寸策略接口 - * - * 定义了控制窗口显示行为(全屏、无边框等)的策略接口。 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 桌面行为标志枚举 - * - * 使用位标志表示窗口的各种行为特性。 - * 多个标志可以使用位运算符组合。 - */ -enum class DesktopBehaviorFlag { - Fullscreen = 0x1, ///< 全屏模式 - Frameless = 0x2, ///< 无边框窗口 - StayOnBottom = 0x4, ///< 置底窗口 - AllowResize = 0x8, ///< 允许调整大小 - AvoidSystemUI = 0x10, ///< 避开系统 UI -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -/** - * @brief 桌面显示尺寸策略接口 - * - * @details 定义了窗口显示行为的操作接口: - * - action(): 应用行为到窗口 - * - query(): 查询当前行为状态 - */ -class IDesktopDisplaySizeStrategy : public IDesktopPropertyStrategy { -public: - virtual ~IDesktopDisplaySizeStrategy() = default; - - /** - * @brief 获取策略的 WeakPtr - * - * @return WeakPtr 指向当前策略实例 - * - * @note 用于避免循环引用 - */ - WeakPtr GetOne() { - return weak_factory_ptr_.GetWeakPtr(); - } - - /** - * @brief 应用窗口行为 - * - * @param widget_data 目标 QWidget 指针 - * @return 成功返回 true,失败返回 false - * - * @note 默认实现不做任何操作,返回 true - * @note 派生类应重写此方法以实现特定行为 - */ - virtual bool action(QWidget* widget_data) { - return true; // 默认:不做任何操作 - } - - /** - * @brief 查询当前窗口行为 - * - * @return 当前行为的标志组合 - * - * @note 默认实现返回 None - * @note 派生类应重写此方法以查询实际状态 - */ - virtual DesktopBehaviors query() const; - -private: - WeakPtrFactory weak_factory_ptr_; -}; - -} // namespace cf::desktop::platform_strategy -```bash - -参考资料: -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [C++ ABI Compatibility - Itanium C++ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html) -- [Pimpl Idiom in C++ - Wikipedia](https://en.cppreference.com/w/cpp/language/pimpl) - ---- - -## Action vs Query:核心思想对比 - -### 4.1 CQRS 原则 - -**CQRS**(Command Query Responsibility Segregation)是 Bertrand Meyer 提出的设计原则,在 Martin Fowler 的文章中得到详细阐述: - -> "CQRS stands for Command Query Responsibility Segregation. It's a pattern that separates operations that read data (queries) from operations that modify data (commands)." - -在我们的策略系统中: - -| 类型 | 方法 | 职责 | 副作用 | 返回值 | -|------|------|------|--------|--------| -| **Action** | `action()` | 修改系统状态 | 有 | bool(成功/失败) | -| **Query** | `query()` | 读取系统状态 | 无 | DesktopBehaviors(状态) | - -### 4.2 Action 方法设计 - -Action 方法遵循以下设计原则: - -#### 1. 明确的副作用 - -```cpp -/** - * @brief 应用窗口行为 - * - * @details 此方法会: - * 1. 修改 widget 的窗口标志(Qt::WindowFlags) - * 2. 可能改变 widget 的显示状态 - * 3. 可能影响 widget 的位置和大小 - * - * @param widget_data 目标 QWidget - * @return true 表示操作成功,false 表示失败 - * - * @note 调用此方法会改变 widget 的状态 - * @warning 不应在持有锁的状态下调用此方法(可能触发 Qt 事件) - */ -virtual bool action(QWidget* widget_data); -```text - -#### 2. 返回值表示操作结果 - -```cpp -// ✅ 正确:使用返回值表示成功/失败 -bool result = strategy->action(widget); -if (!result) { - // 处理失败情况 - qWarning() << "Failed to apply strategy:" << strategy->name(); -} - -// ❌ 错误:在 action 中抛出异常(Qt 约定不使用异常) -virtual bool action(QWidget* widget_data) { - if (!widget_data) { - throw std::invalid_argument("widget is null"); // 不推荐 - } -} -```text - -#### 3. 幂等性 - -Action 方法应该是幂等的,多次调用应该产生相同的结果: - -```cpp -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - bool action(QWidget* widget) override { - if (!widget) return false; - - // 幂等性检查:如果已经是全屏,不需要重复操作 - if (widget->isFullScreen()) { - return true; - } - - widget->showFullScreen(); - return true; - } -}; -```text - -### 4.3 Query 方法设计 - -Query 方法遵循以下设计原则: - -#### 1. 无副作用 - -```cpp -/** - * @brief 查询当前窗口行为 - * - * @details 此方法: - * 1. 不修改任何状态 - * 2. 不触发任何事件 - * 3. 可以安全地在任何上下文中调用 - * - * @return 当前行为的标志组合 - * - * @note 此方法是线程安全的(假设 DesktopBehaviors 是值类型) - * @note 可以多次调用而不影响系统状态 - */ -virtual DesktopBehaviors query() const; -```text - -#### 2. 返回完整信息 - -```cpp -// ✅ 正确:返回完整的状态信息 -DesktopBehaviors behaviors = strategy->query(); -if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 处理全屏状态 -} - -// ❌ 错误:提供多个查询方法 -bool isFullscreen() const; // 应该使用 query().testFlag() -bool isFrameless() const; // 应该使用 query().testFlag() -```text - -#### 3. const 正确性 - -```cpp -// Query 方法必须是 const 的 -virtual DesktopBehaviors query() const; - -// 允许在 const 上下文中调用 -void monitorDesktop(const IDesktopDisplaySizeStrategy& strategy) { - DesktopBehaviors current = strategy.query(); // OK -} -```text - -### 4.4 分离的好处 - -#### 1. 缓存优化 - -Query 方法可以被安全地缓存: - -```cpp -class CachedDesktopDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - DesktopBehaviors query() const override { - // 缓存结果,避免重复查询 - if (!cache_valid_) { - cached_behaviors_ = doQuery(); - cache_valid_ = true; - } - return cached_behaviors_; - } - - bool action(QWidget* widget) override { - // Action 操作后使缓存失效 - bool result = doAction(widget); - cache_valid_ = false; - return result; - } - -private: - mutable DesktopBehaviors cached_behaviors_; - mutable bool cache_valid_ = false; -}; -```text - -#### 2. 并发访问 - -Query 方法可以被多个线程同时调用: - -```cpp -// 线程 1:读取状态 -DesktopBehaviors state1 = strategy.query(); - -// 线程 2:同时读取状态(无竞态条件) -DesktopBehaviors state2 = strategy.query(); - -// 线程 3:修改状态(需要同步) -std::lock_guard lock(mtx); -strategy.action(widget); -```text - -#### 3. 前置条件检查 - -在执行 Action 之前检查状态: - -```cpp -// 检查当前状态后再决定是否执行 action -DesktopBehaviors current = strategy.query(); -if (!current.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 只有在非全屏状态下才执行全屏操作 - strategy.action(widget); -} -```yaml - -参考资料: -- [CQRS - Martin Fowler](https://martinfowler.com/bliki/CQRS.html) -- [Command-Query Separation - Wikipedia](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) -- [CQRS Pattern - Microsoft Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) - ---- - -## 示例策略实现 - -### 5.1 FullscreenStrategy - -全屏策略负责将窗口设置为全屏模式: - -```cpp -/** - * @file FullscreenStrategy.h - * @brief 全屏策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 全屏策略 - * - * @details 将窗口设置为全屏模式,隐藏标题栏和边框 - */ -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - FullscreenStrategy() = default; - ~FullscreenStrategy() override = default; - - /** - * @brief 获取策略名称 - */ - const char* name() const noexcept override { - return "Fullscreen Strategy"; - } - - /** - * @brief 应用全屏模式 - * - * @param widget 目标窗口 - * @return 成功返回 true - */ - bool action(QWidget* widget) override { - if (!widget) { - qWarning() << "FullscreenStrategy: null widget"; - return false; - } - - // 幂等性检查 - if (widget->isFullScreen()) { - qDebug() << "Widget already in fullscreen mode"; - return true; - } - - // 应用全屏 - widget->showFullScreen(); - - qDebug() << "Applied fullscreen to" << widget; - return true; - } - - /** - * @brief 查询当前状态 - */ - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen | - DesktopBehaviorFlag::Frameless; - } -}; - -} // namespace -```text - -### 5.2 FramelessStrategy - -无边框策略负责移除窗口的标题栏和边框: - -```cpp -/** - * @file FramelessStrategy.h - * @brief 无边框策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 无边框策略 - * - * @details 移除窗口的标题栏和边框,通常配合自定义标题栏使用 - */ -class FramelessStrategy : public IDesktopDisplaySizeStrategy { -public: - FramelessStrategy() = default; - ~FramelessStrategy() override = default; - - const char* name() const noexcept override { - return "Frameless Strategy"; - } - - bool action(QWidget* widget) override { - if (!widget) { - qWarning() << "FramelessStrategy: null widget"; - return false; - } - - // 获取当前窗口标志 - Qt::WindowFlags flags = widget->windowFlags(); - - // 幂等性检查 - if (flags & Qt::FramelessWindowHint) { - qDebug() << "Widget already frameless"; - return true; - } - - // 添加无边框标志 - flags |= Qt::FramelessWindowHint; - - // 设置窗口标志(会触发窗口重建) - widget->setWindowFlags(flags); - - // 显示窗口(setWindowFlags 会隐藏窗口) - widget->show(); - - qDebug() << "Applied frameless to" << widget; - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Frameless; - } -}; - -} // namespace -```text - -### 5.3 WSL 平台策略 - -WSL(Windows Subsystem for Linux)平台的特殊实现: - -```cpp -/** - * @file linux_wsl_display_size_policy.h - * @brief WSL 平台显示策略 - */ - -namespace cf::desktop::platform_strategy::wsl { - -/** - * @brief WSL 显示尺寸策略 - * - * @details WSL 环境下的特殊实现,需要处理 Windows/WSL 边界 - */ -class DisplaySizePolicyMaker : public IDesktopDisplaySizeStrategy { -public: - DisplaySizePolicyMaker(); - ~DisplaySizePolicyMaker(); - - const char* name() const noexcept override { - return "WSL Desktop Size Policy"; - } - - /** - * @brief 应用 WSL 特定的窗口行为 - * - * @details WSL 环境下: - * 1. 需要检测是否在 WSL 内运行 - * 2. 某些窗口标志可能不可用 - * 3. 可能需要通过 WSLg (WSL GUI) 处理 - */ - bool action(QWidget* widget_data) override { - if (!widget_data) { - return false; - } - - // WSL 特定逻辑 - #ifdef Q_OS_WIN - // 运行在 Windows 上,可能需要特殊处理 - #elif defined(Q_OS_LINUX) - // 检测 WSL 环境 - if (isWSLEnvironment()) { - return applyWSLBehavior(widget_data); - } - #endif - - // 默认行为 - return true; - } - - /** - * @brief 查询 WSL 支持的行为 - */ - DesktopBehaviors query() const override { - DesktopBehaviors behaviors = DesktopBehaviorFlag::None; - - #ifdef Q_OS_WIN - // Windows 上支持全屏 - behaviors |= DesktopBehaviorFlag::Fullscreen; - #elif defined(Q_OS_LINUX) - if (isWSLEnvironment()) { - // WSL 限制支持 - behaviors |= DesktopBehaviorFlag::Frameless; - behaviors |= DesktopBehaviorFlag::AllowResize; - } - #endif - - return behaviors; - } - -private: - /** - * @brief 检测是否在 WSL 环境中运行 - */ - bool isWSLEnvironment() const { - // 检查 /proc/version 是否包含 "Microsoft" - QFile versionFile("/proc/version"); - if (versionFile.open(QIODevice::ReadOnly)) { - QByteArray content = versionFile.readAll(); - return content.contains("Microsoft"); - } - return false; - } - - /** - * @brief 应用 WSL 特定的窗口行为 - */ - bool applyWSLBehavior(QWidget* widget) { - // WSLg 特定实现 - // ... - - return true; - } -}; - -} // namespace cf::desktop::platform_strategy::wsl -```text - -### 5.4 组合行为策略 - -一个策略可以组合多个行为标志: - -```cpp -/** - * @brief 桌面覆盖层策略 - * - * @details 实现类似 Windows 小部件的桌面覆盖层: - * - 无边框 - * - 置底 - * - 避开系统 UI - * - 不接受键盘输入 - */ -class DesktopOverlayStrategy : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Desktop Overlay Strategy"; - } - - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - Qt::WindowFlags flags = widget->windowFlags(); - - // 移除冲突标志 - flags &= ~Qt::WindowStaysOnTopHint; - - // 添加所需标志 - flags |= Qt::FramelessWindowHint; - flags |= Qt::WindowStaysOnBottomHint; - flags |= Qt::WindowDoesNotAcceptFocus; - - widget->setWindowFlags(flags); - widget->show(); - - return true; - } - - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Frameless | - DesktopBehaviorFlag::StayOnBottom | - DesktopBehaviorFlag::AvoidSystemUI; - } -}; -```yaml - -参考资料: -- [QWidget::setWindowFlags - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop) -- [QWidget::showFullScreen - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#showFullScreen) -- [WSL GUI (WSLg) Documentation - Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) - ---- +Strategy 模式将这些平台特定的行为实现封装为独立的策略类,通过统一接口 `IDesktopDisplaySizeStrategy` 暴露给上层。Context(如 CFDesktop 主窗口)只依赖抽象接口,不关心具体平台实现。这带来了四个直接好处:(1) 新平台只需新增策略类,符合开闭原则;(2) 每个策略类只负责单一平台的单一行为维度,符合单一职责原则;(3) 策略可通过工厂 + DLL 插件化加载;(4) 单元测试中可用 Mock 策略替换真实实现。 -## 多策略组合实现 +## 关键决策 -### 6.1 策略组合模式 +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| Strategy 模式封装平台行为 | 消除条件分支地狱、支持开闭原则 | `#ifdef` 全局分支(不可测试、不可扩展) | +| 接口继承链: `IDesktopPropertyStrategy` -> `IDesktopDisplaySizeStrategy` | 基类提供 `StrategyType` 枚举和 `name()` ABI 标识;子类添加 `action()`/`query()` | 扁平单接口(无法区分 DisplaySizePolicy vs Extensions) | +| CQRS: `action(QWidget*) -> bool` + `query() const -> DesktopBehaviors` | Action 有副作用(修改窗口状态)、Query 无副作用(纯读取);分离后 Query 可缓存、可并发调用 | 单一 `apply()` 方法混合读写(无法安全缓存或并发) | +| `WeakPtrFactory` 集成 | 策略可能被多个组件引用,WeakPtr 避免循环引用和悬挂指针 | 原始指针(悬挂风险)、shared_ptr(循环引用风险) | +| 工厂模式 `PlatformFactory` + 自定义删除器 `StrategyDeleter` | 确保跨 DLL 边界的正确分配/释放;`factorize_unique()` / `factorize_shared()` 满足不同所有权需求 | 直接 `new/delete`(跨 DLL 不安全) | +| 冲突检测: `StayOnTop` vs `StayOnBottom` 互斥、`Fullscreen` vs `AllowResize` 冲突 | 某些行为标志语义上不能共存,需在应用前检测并按优先级自动解决 | 静默允许冲突(运行时未定义行为) | +| Action 方法幂等性要求 | `widget->isFullScreen()` 前置检查避免重复操作触发不必要的窗口重建事件 | 无幂等保证(多次调用可能闪烁或丢失状态) | -单个策略往往不足以应对复杂的场景,需要组合多个策略: - -```cpp -/** - * @brief 策略组合接口 - * - * @details 允许将多个策略组合成一个复合策略 - */ -class ICompositeStrategy : public IDesktopDisplaySizeStrategy { -public: - /** - * @brief 添加子策略 - * - * @param strategy 要添加的策略 - * @return 成功返回 true - */ - virtual bool addStrategy(std::shared_ptr strategy) = 0; - - /** - * @brief 移除子策略 - * - * @param strategy 要移除的策略 - * @return 成功返回 true - */ - virtual bool removeStrategy(IDesktopDisplaySizeStrategy* strategy) = 0; - - /** - * @brief 获取子策略数量 - */ - virtual size_t strategyCount() const = 0; - - /** - * @brief 清空所有子策略 - */ - virtual void clearStrategies() = 0; -}; -```text - -### 6.2 CompositeStrategy - -复合策略的实现: - -```cpp -/** - * @file CompositeStrategy.h - * @brief 复合策略实现 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 复合策略 - * - * @details 将多个策略组合成一个,按顺序执行所有策略 - */ -class CompositeStrategy : public ICompositeStrategy { -public: - CompositeStrategy() = default; - ~CompositeStrategy() override = default; - - const char* name() const noexcept override { - return "Composite Strategy"; - } - - bool addStrategy(std::shared_ptr strategy) override { - if (!strategy) { - return false; - } - - strategies_.push_back(strategy); - return true; - } - - bool removeStrategy(IDesktopDisplaySizeStrategy* strategy) override { - auto it = std::remove_if(strategies_.begin(), strategies_.end(), - [strategy](const auto& ptr) { - return ptr.get() == strategy; - }); - - if (it != strategies_.end()) { - strategies_.erase(it, strategies_.end()); - return true; - } - return false; - } - - size_t strategyCount() const override { - return strategies_.size(); - } - - void clearStrategies() override { - strategies_.clear(); - } - - /** - * @brief 应用所有子策略 - * - * @details 按添加顺序执行所有策略 - * 如果任一策略失败,继续执行后续策略 - */ - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - bool all_success = true; - - for (auto& strategy : strategies_) { - if (!strategy->action(widget)) { - qWarning() << "Strategy" << strategy->name() << "failed"; - all_success = false; - } - } - - return all_success; - } - - /** - * @brief 查询所有子策略的行为组合 - * - * @details 将所有子策略的行为标志组合在一起 - */ - DesktopBehaviors query() const override { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - - for (const auto& strategy : strategies_) { - combined |= strategy->query(); - } - - return combined; - } - -private: - std::vector> strategies_; -}; - -} // namespace -```text - -### 6.3 策略链模式 - -策略链模式允许策略按链式执行,每个策略可以决定是否继续传递: - -```cpp -/** - * @brief 策略链上下文 - * - * @details 包含策略执行过程中的状态信息 - */ -struct StrategyChainContext { - QWidget* widget; ///< 目标窗口 - bool stop_processing; ///< 是否停止后续策略 - DesktopBehaviors applied; ///< 已应用的行为 - QString error_message; ///< 错误消息 - - StrategyChainContext(QWidget* w) - : widget(w), stop_processing(false), applied(DesktopBehaviorFlag::None) {} -}; - -/** - * @brief 链式策略接口 - * - * @details 每个策略可以决定是否继续执行后续策略 - */ -class IChainStrategy : public IDesktopDisplaySizeStrategy { -public: - /** - * @brief 执行策略(链式) - * - * @param widget 目标窗口 - * @param context 链上下文 - * @return 如果设置 context.stop_processing,停止后续策略 - */ - virtual bool process(QWidget* widget, StrategyChainContext& context) = 0; -}; - -/** - * @brief 策略链 - * - * @details 按顺序执行链中的策略,支持提前终止 - */ -class StrategyChain : public IDesktopDisplaySizeStrategy { -public: - const char* name() const noexcept override { - return "Strategy Chain"; - } - - void addStrategy(std::shared_ptr strategy) { - chain_.push_back(strategy); - } - - bool action(QWidget* widget) override { - if (!widget) { - return false; - } - - StrategyChainContext context(widget); - - for (auto& strategy : chain_) { - if (!strategy->process(widget, context)) { - // 策略失败 - qWarning() << "Chain strategy" << strategy->name() << "failed"; - } - - // 检查是否需要停止 - if (context.stop_processing) { - qDebug() << "Chain stopped at" << strategy->name() - << "reason:" << context.error_message; - break; - } - } - - return true; - } - - DesktopBehaviors query() const override { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - for (const auto& strategy : chain_) { - combined |= strategy->query(); - } - return combined; - } - -private: - std::vector> chain_; -}; -```text - -### 6.4 条件策略 - -根据条件选择不同的策略: - -```cpp -/** - * @brief 条件策略 - * - * @details 根据运行时条件选择不同的策略 - */ -class ConditionalStrategy : public IDesktopDisplaySizeStrategy { -public: - using ConditionFunc = std::function; - - ConditionalStrategy( - ConditionFunc condition, - std::shared_ptr true_strategy, - std::shared_ptr false_strategy - ) : condition_(condition), - true_strategy_(true_strategy), - false_strategy_(false_strategy) {} - - const char* name() const noexcept override { - return "Conditional Strategy"; - } - - bool action(QWidget* widget) override { - auto strategy = condition_() ? true_strategy_ : false_strategy_; - if (!strategy) { - return false; - } - return strategy->action(widget); - } - - DesktopBehaviors query() const override { - auto strategy = condition_() ? true_strategy_ : false_strategy_; - if (!strategy) { - return DesktopBehaviorFlag::None; - } - return strategy->query(); - } - -private: - ConditionFunc condition_; - std::shared_ptr true_strategy_; - std::shared_ptr false_strategy_; -}; - -// 使用示例 -auto createConditionalStrategy() { - return std::make_shared( - []() { - // 条件:检测是否在 Wayland 上运行 - return QGuiApplication::platformName() == "wayland"; - }, - std::make_shared(), // true 分支 - std::make_shared() // false 分支 - ); -} -```yaml - -参考资料: -- [Composite Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/composite) -- [Chain of Responsibility Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/chain-of-responsibility) - ---- - -## 冲突检测机制 - -### 7.1 行为冲突定义 - -某些行为标志之间存在互斥关系,不能同时应用: - -```cpp -/** - * @brief 行为冲突规则 - * - * @details 定义了行为标志之间的冲突关系 - */ -namespace BehaviorConflictRules { - -/** - * @brief 冲突规则定义 - */ -struct ConflictRule { - DesktopBehaviors flags; ///< 要检查的标志组合 - DesktopBehaviors conflicts_with; ///< 与之冲突的标志 - const char* reason; ///< 冲突原因说明 -}; - -/** - * @brief 定义的所有冲突规则 - */ -inline const std::vector kConflictRules = { - // 置顶和置底冲突 - { - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - "Window cannot stay on both top and bottom" - }, - // 全屏模式下通常不允许调整大小 - { - DesktopBehaviorFlag::Fullscreen, - DesktopBehaviorFlag::AllowResize, - "Fullscreen windows typically don't allow resize" - }, - // 避开系统 UI 通常需要无边框 - { - DesktopBehaviorFlag::AvoidSystemUI, - DesktopBehaviorFlag::None, // 不与其他标志冲突 - "AvoidSystemUI is typically used with Frameless" - }, -}; - -} // namespace BehaviorConflictRules -```text - -### 7.2 冲突检测接口 - -```cpp -/** - * @brief 冲突检测结果 - */ -struct ConflictDetectionResult { - bool has_conflicts; ///< 是否存在冲突 - DesktopBehaviors conflicts; ///< 冲突的标志 - std::vector reasons; ///< 冲突原因列表 - - ConflictDetectionResult() - : has_conflicts(false), conflicts(DesktopBehaviorFlag::None) {} - - void addConflict(DesktopBehaviorFlag flag, const char* reason) { - has_conflicts = true; - conflicts |= flag; - reasons.push_back(reason); - } - - QString toString() const { - if (!has_conflicts) { - return "No conflicts"; - } - - QString result = "Conflicts detected:\n"; - for (const auto& reason : reasons) { - result += " - "; - result += reason; - result += "\n"; - } - return result; - } -}; - -/** - * @brief 冲突检测器 - */ -class StrategyConflictDetector { -public: - /** - * @brief 检测行为冲突 - * - * @param behaviors 要检测的行为组合 - * @return 冲突检测结果 - */ - static ConflictDetectionResult detect(DesktopBehaviors behaviors) { - ConflictDetectionResult result; - - for (const auto& rule : BehaviorConflictRules::kConflictRules) { - // 检查是否包含规则中的标志 - if ((behaviors & rule.flags).operator bool()) { - // 检查是否同时包含冲突的标志 - if ((behaviors & rule.conflicts_with).operator bool()) { - result.addConflict(rule.conflicts_with, rule.reason); - } - } - } - - return result; - } - - /** - * @brief 检测策略列表冲突 - * - * @param strategies 策略列表 - * @return 冲突检测结果 - */ - static ConflictDetectionResult detect( - const std::vector>& strategies - ) { - DesktopBehaviors combined = DesktopBehaviorFlag::None; - - for (const auto& strategy : strategies) { - combined |= strategy->query(); - } - - return detect(combined); - } -}; -```text - -### 7.3 冲突解决策略 - -```cpp -/** - * @brief 冲突解决策略枚举 - */ -enum class ConflictResolution { - Fail, ///< 失败,不应用任何行为 - Warning, ///< 警告,应用行为但记录警告 - AutoResolve, ///< 自动解决,移除冲突的行为 - Prioritize ///< 优先级,使用预定义的优先级 -}; - -/** - * @brief 冲突解决器 - */ -class StrategyConflictResolver { -public: - /** - * @brief 解决冲突 - * - * @param behaviors 原始行为 - * @param resolution 解决策略 - * @return 解决后的行为 - */ - static DesktopBehaviors resolve( - DesktopBehaviors behaviors, - ConflictResolution resolution = ConflictResolution::Warning - ) { - auto detection = StrategyConflictDetector::detect(behaviors); - - if (!detection.has_conflicts) { - return behaviors; - } - - switch (resolution) { - case ConflictResolution::Fail: - qWarning() << "Conflict detection failed:" << detection.toString(); - return DesktopBehaviorFlag::None; - - case ConflictResolution::Warning: - qWarning() << "Conflicts detected (applying anyway):" - << detection.toString(); - return behaviors; - - case ConflictResolution::AutoResolve: - return autoResolve(behaviors, detection); - - case ConflictResolution::Prioritize: - return prioritizeResolve(behaviors); - } - - return behaviors; - } - -private: - /** - * @brief 自动解决冲突 - * - * @details 移除冲突的行为标志 - */ - static DesktopBehaviors autoResolve( - DesktopBehaviors behaviors, - const ConflictDetectionResult& detection - ) { - DesktopBehaviors resolved = behaviors; - - // 移除所有冲突的标志 - resolved &= ~detection.conflicts; - - qInfo() << "Auto-resolved conflicts, removed:" << detection.toString(); - - return resolved; - } - - /** - * @brief 优先级解决 - * - * @details 根据预定义的优先级解决冲突 - */ - static DesktopBehaviors prioritizeResolve(DesktopBehaviors behaviors) { - // 定义优先级(高到低) - const std::vector priority = { - DesktopBehaviorFlag::Fullscreen, // 最高优先级 - DesktopBehaviorFlag::Frameless, - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - DesktopBehaviorFlag::AvoidSystemUI, - DesktopBehaviorFlag::AllowResize, // 最低优先级 - }; - - // 按优先级检查冲突,保留高优先级的行为 - for (size_t i = 0; i < priority.size(); ++i) { - for (size_t j = i + 1; j < priority.size(); ++j) { - // 检查 i 和 j 是否冲突 - if (behaviors.testFlag(priority[i]) && - behaviors.testFlag(priority[j])) { - - // 检查是否定义了冲突规则 - auto rules = BehaviorConflictRules::kConflictRules; - for (const auto& rule : rules) { - if ((rule.flags & priority[i]).operator bool() || - (rule.conflicts_with & priority[j]).operator bool()) { - // 移除低优先级的行为 - behaviors &= ~priority[j]; - qInfo() << "Prioritized" << static_cast(priority[i]) - << "over" << static_cast(priority[j]); - } - } - } - } - } - - return behaviors; - } -}; -```text - -### 7.4 集成到策略应用 - -```cpp -/** - * @brief 带冲突检测的策略应用器 - */ -class SafeStrategyApplier { -public: - /** - * @brief 应用策略(带冲突检测) - * - * @param widget 目标窗口 - * @param strategy 要应用的策略 - * @param resolution 冲突解决策略 - * @return 成功返回 true - */ - static bool apply( - QWidget* widget, - IDesktopDisplaySizeStrategy* strategy, - ConflictResolution resolution = ConflictResolution::Warning - ) { - if (!widget || !strategy) { - return false; - } - - // 查询行为 - DesktopBehaviors behaviors = strategy->query(); - - // 检测冲突 - auto detection = StrategyConflictDetector::detect(behaviors); - - if (detection.has_conflicts) { - qWarning() << "Strategy" << strategy->name() - << "has conflicts:" << detection.toString(); - - // 解决冲突 - behaviors = StrategyConflictResolver::resolve(behaviors, resolution); - - if (behaviors == DesktopBehaviorFlag::None && - resolution == ConflictResolution::Fail) { - return false; - } - } - - // 应用策略 - return strategy->action(widget); - } -}; -```yaml - -参考资料: -- [Conflict-Free Replicated Data Types (CRDTs) - Wikipedia](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) -- [Semantic Conflict Resolution - Microsoft Research](https://www.microsoft.com/en-us/research/publication/semantic-conflict-resolution/) - ---- - -## 工厂模式集成 - -### 8.1 PlatformFactory 设计 - -工厂模式与策略模式结合,提供策略的创建和管理: - -```cpp -/** - * @file DesktopPropertyStrategyFactory.h - * @brief 桌面属性策略工厂 - */ - -namespace cf::desktop::platform_strategy { - -/** - * @brief 平台工厂 - * - * @details 负责创建和管理平台特定的策略实例 - */ -class PlatformFactory { -public: - PlatformFactory(); - ~PlatformFactory(); - - /** - * @brief 策略删除器类型 - * - * @details 使用自定义删除器确保正确的内存管理 - */ - using StrategyDeleter = void (*)(IDesktopPropertyStrategy*); - - /** - * @brief 创建策略(unique_ptr 版本) - * - * @param t 策略类型 - * @return 策略的 unique_ptr,使用自定义删除器 - * - * @note 返回的策略调用者拥有所有权 - * @note 使用工厂的删除器确保跨 DLL 边界的安全释放 - */ - std::unique_ptr - factorize_unique(const IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 创建策略(shared_ptr 版本) - * - * @param t 策略类型 - * @return 策略的 shared_ptr - * - * @note 允许多个调用者共享策略所有权 - * @note 使用工厂的删除器确保正确的释放 - */ - std::shared_ptr - factorize_shared(const IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 检查策略类型是否可用 - * - * @param t 策略类型 - * @return 可用返回 true - */ - bool isAvailable(const IDesktopPropertyStrategy::StrategyType t) const; - -private: - class PlatformImpl; - std::unique_ptr impl_; -}; - -} // namespace -```text - -### 8.2 平台特定工厂 - -每个平台可以提供自己的工厂实现: - -```cpp -/** - * @file linux_wsl_factory.h - * @brief WSL 平台特定工厂 - */ - -namespace cf::desktop::platform_strategy::wsl { - -/** - * @brief WSL 桌面属性策略工厂 - * - * @details WSL 平台的策略工厂实现 - */ -class WSLDeskProStrategyFactory : public SimpleSingleton { -public: - WSLDeskProStrategyFactory(); - ~WSLDeskProStrategyFactory(); - - /** - * @brief 创建策略 - * - * @param t 策略类型 - * @return 策略指针,由工厂管理生命周期 - * - * @note 调用者不应手动删除返回的策略 - * @note 使用 release() 释放策略 - */ - IDesktopPropertyStrategy* create(IDesktopPropertyStrategy::StrategyType t); - - /** - * @brief 释放策略 - * - * @param ptr 要释放的策略指针 - * - * @note 只有通过 create() 创建的策略才能通过此方法释放 - */ - void release(IDesktopPropertyStrategy* ptr); - -private: - std::unique_ptr sz_policy; -}; - -} // namespace -```text - -### 8.3 插件化工厂 - -支持从动态库加载策略: - -```cpp -/** - * @brief 插件策略工厂 - * - * @details 从动态库加载策略实现 - */ -class PluginStrategyFactory { -public: - /** - * @brief 加载插件 - * - * @param plugin_path 插件路径 - * @return 成功返回 true - * - * @note 插件必须实现 PluginStrategyInterface - */ - bool loadPlugin(const QString& plugin_path); - - /** - * @brief 卸载插件 - * - * @param plugin_path 插件路径 - */ - void unloadPlugin(const QString& plugin_path); - - /** - * @brief 从插件创建策略 - * - * @param plugin_path 插件路径 - * @param strategy_name 策略名称 - * @return 策略指针 - */ - std::shared_ptr - createFromPlugin(const QString& plugin_path, const QString& strategy_name); - - /** - * @brief 获取所有可用的插件 - */ - QStringList availablePlugins() const; - -private: - struct PluginInfo { - QPluginLoader* loader; - QString name; - QString version; - QStringList strategies; - }; - - QHash plugins_; -}; - -/** - * @brief 插件策略接口 - * - * @details 所有插件策略必须实现此接口 - */ -class PluginStrategyInterface { -public: - virtual ~PluginStrategyInterface() = default; - - /** - * @brief 插件名称 - */ - virtual QString pluginName() const = 0; - - /** - * @brief 插件版本 - */ - virtual QString pluginVersion() const = 0; - - /** - * @brief 创建策略 - * - * @param strategy_name 策略名称 - * @return 策略指针 - */ - virtual IDesktopPropertyStrategy* createStrategy(const QString& strategy_name) = 0; - - /** - * @brief 获取支持的策略列表 - */ - virtual QStringList supportedStrategies() const = 0; -}; -```yaml - -参考资料: -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [Factory Method Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/factory-method) -- [Abstract Factory Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/abstract-factory) - ---- - -## 最佳实践与设计原则 - -### 9.1 策略设计原则 - -#### 1. 单一职责 - -每个策略应该只负责一个明确的行为: - -```cpp -// ✅ 正确:单一职责 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { - // 只负责全屏行为 -}; - -// ❌ 错误:多重职责 -class WindowStrategy : public IDesktopDisplaySizeStrategy { - // 同时负责全屏、无边框、置顶等多个行为 - bool action(QWidget* widget) override { - widget->showFullScreen(); - widget->setWindowFlags(widget->windowFlags() | Qt::FramelessWindowHint); - // ... 更多行为 - } -}; -```text - -#### 2. 开闭原则 - -对扩展开放,对修改封闭: - -```cpp -// ✅ 正确:通过添加新策略扩展功能 -class NewBehaviorStrategy : public IDesktopDisplaySizeStrategy { - // 新功能通过新策略实现,不修改现有代码 -}; - -// ❌ 错误:修改现有策略添加新功能 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { - bool action(QWidget* widget) override { - widget->showFullScreen(); - // 添加了与全屏无关的新功能 - addNewFeature(widget); // 违反开闭原则 - } -}; -```text - -#### 3. 依赖倒置 - -依赖抽象而非具体实现: - -```cpp -// ✅ 正确:依赖抽象接口 -class CFDesktop { -private: - IDesktopDisplaySizeStrategy* display_strategy_; // 抽象接口 -}; - -// ❌ 错误:依赖具体实现 -class CFDesktop { -private: - FullscreenStrategy* display_strategy_; // 具体实现 -}; -```text - -### 9.2 命名约定 - -```cpp -// 策略接口命名:I + 功能 + Strategy -class IDesktopDisplaySizeStrategy; - -// 具体策略命名:功能 + Strategy -class FullscreenStrategy; -class FramelessStrategy; - -// 平台特定策略:平台 + 功能 + Strategy -class WSLDisplaySizePolicy; -class WindowsDisplaySizePolicy; - -// 工厂命名:功能 + Factory -class PlatformFactory; -class StrategyFactory; - -// 组合策略:Composite + 功能 -class CompositeStrategy; -class StrategyChain; -```text - -### 9.3 错误处理 - -```cpp -/** - * @brief 策略错误类型 - */ -enum class StrategyError { - None, ///< 无错误 - NullWidget, ///< 空指针 - NotSupported, ///< 不支持的操作 - Conflict, ///< 行为冲突 - Failed ///< 操作失败 -}; - -/** - * @brief 策略结果 - */ -struct StrategyResult { - bool success; ///< 成功标志 - StrategyError error; ///< 错误类型 - QString message; ///< 错误消息 - - static StrategyResult ok() { - return {true, StrategyError::None, QString()}; - } - - static StrategyResult fail(StrategyError error, const QString& msg) { - return {false, error, msg}; - } -}; - -// 在策略中使用 -class FullscreenStrategy : public IDesktopDisplaySizeStrategy { -public: - bool action(QWidget* widget) override { - if (!widget) { - last_result_ = StrategyResult::fail( - StrategyError::NullWidget, - "Widget pointer is null" - ); - return false; - } - - // ... 执行操作 - - last_result_ = StrategyResult::ok(); - return true; - } - - StrategyResult getLastResult() const { - return last_result_; - } - -private: - StrategyResult last_result_; -}; -```text - -### 9.4 性能考虑 - -```cpp -/** - * @brief 缓存策略结果 - */ -class CachedDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - CachedDisplaySizeStrategy(std::shared_ptr impl) - : impl_(impl), cache_valid_(false) {} - - bool action(QWidget* widget) override { - bool result = impl_->action(widget); - - // action 后使缓存失效 - cache_valid_ = false; - - return result; - } - - DesktopBehaviors query() const override { - if (cache_valid_) { - return cached_behaviors_; - } - - cached_behaviors_ = impl_->query(); - cache_valid_ = true; - - return cached_behaviors_; - } - -private: - std::shared_ptr impl_; - mutable DesktopBehaviors cached_behaviors_; - mutable bool cache_valid_; -}; -```text - -### 9.5 线程安全 - -```cpp -/** - * @brief 线程安全的策略 - */ -class ThreadSafeDisplaySizeStrategy : public IDesktopDisplaySizeStrategy { -public: - ThreadSafeDisplaySizeStrategy(std::shared_ptr impl) - : impl_(impl) {} - - bool action(QWidget* widget) override { - std::lock_guard lock(mutex_); - return impl_->action(widget); - } - - DesktopBehaviors query() const override { - std::lock_guard lock(mutex_); - return impl_->query(); - } - -private: - std::shared_ptr impl_; - mutable std::mutex mutex_; -}; -```text - -### 9.6 测试策略 - -```cpp -// 测试辅助类 -class StrategyTestHelper { -public: - /** - * @brief 创建测试窗口 - */ - static std::unique_ptr createTestWidget() { - auto widget = std::make_unique(); - widget->resize(800, 600); - return widget; - } - - /** - * @brief 验证策略结果 - */ - static bool verifyStrategyResult( - IDesktopDisplaySizeStrategy* strategy, - const DesktopBehaviors& expected - ) { - DesktopBehaviors actual = strategy->query(); - return actual == expected; - } -}; - -// 测试用例 -TEST(FullscreenStrategyTest, ApplyFullscreen) { - FullscreenStrategy strategy; - auto widget = StrategyTestHelper::createTestWidget(); - - // 应用策略 - bool result = strategy.action(widget.get()); - EXPECT_TRUE(result); - - // 验证结果 - EXPECT_TRUE(StrategyTestHelper::verifyStrategyResult( - &strategy, - DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless - )); -} -```yaml - ---- - -## 总结 - -### 核心要点 - -1. **Strategy Pattern 适用场景**: - - 跨平台差异处理 - - 行为解耦与单一职责 - - 插件化架构支持 - - 测试友好设计 - -2. **Action vs Query 分离**: - - Action:修改状态,有副作用 - - Query:读取状态,无副作用 - - 遵循 CQRS 原则 - -3. **接口设计**: - - 虚析构函数 - - ABI 友好设计 - - WeakPtr 支持 - - 自定义删除器 - -4. **多策略组合**: - - Composite Pattern - - Chain of Responsibility - - 条件策略 - -5. **冲突检测**: - - 定义冲突规则 - - 自动检测冲突 - - 多种解决策略 - -### 参考资源 - -#### 官方文档 -- [Qt 6.10.2 QFlags 官方文档](https://doc.qt.io/qt-6/qflags.html) -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [QWidget::setWindowFlags - Qt Documentation](https://doc.qt.io/qt-6/qwidget.html#windowFlags-prop) - -#### 设计模式参考 -- [Strategy in C++ / Design Patterns - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy/cpp/example) -- [The Strategy Pattern – MC++ BLOG - Modernes C++](https://www.modernescpp.com/index.php/the-strategy-pattern/) -- [Design Patterns: Elements of Reusable Object-Oriented Software - GoF](https://en.wikipedia.org/wiki/Design_Patterns) - -#### CQRS 原则 -- [CQRS - Martin Fowler](https://martinfowler.com/bliki/CQRS.html) -- [Command-Query Separation - Wikipedia](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) -- [CQRS Pattern - Microsoft Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) - -#### 跨平台开发 -- [Best Practices for "Cross-Platform" Development with Qt - Stack Overflow](https://stackoverflow.com/questions/4839350/best-practices-for-cross-platform-development-with-qt) -- [Wayland and Qt - Qt 6.11 文档](https://doc.qt.io/qt-6/wayland-and-qt.html) -- [WSL GUI (WSLg) Documentation - Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) - ---- +## 当前状态 -**文档版本**: 1.0 -**最后更新**: 2026-03-27 -**作者**: CFDesktop Team -**许可**: MIT License +已实现。接口定义位于 `desktop/ui/platform/IDesktopDisplaySizeStrategy.h` 和 `IDesktopPropertyStrategy.h`。平台实现位于 `desktop/ui/platform/windows/`(Windows)和 `desktop/ui/platform/linux_wsl/`(WSL/X11)。工厂位于 `desktop/ui/platform/DesktopPropertyStrategyFactory.{h,cpp}`。 diff --git a/document/notes/04-Desktop-Behavior-System-Architecture.md b/document/notes/04-Desktop-Behavior-System-Architecture.md index c802dbeea..7747d2e15 100644 --- a/document/notes/04-Desktop-Behavior-System-Architecture.md +++ b/document/notes/04-Desktop-Behavior-System-Architecture.md @@ -1,1733 +1,30 @@ --- title: 桌面行为系统设计:从策略到Window Manager抽象 -description: 桌面行为系统设计:从策略到Window Manager抽象 的详细文档 +description: 桌面行为系统的分层架构、行为流转管线、冲突解决与插件化策略的设计意图 --- -# 桌面行为系统设计:从策略到Window Manager抽象 +# 桌面行为系统设计:从策略到Window Manager抽象 -- 设计意图 -## 目录 +## 为什么选择这种方案 -1. [系统架构概览](#系统架构概览) -2. [分层架构设计](#分层架构设计) -3. [行为流转流程](#行为流转流程) -4. [冲突解决机制](#冲突解决机制) -5. [插件系统集成](#插件系统集成) -6. [未来扩展方向](#未来扩展方向) -7. [最佳实践总结](#最佳实践总结) +桌面行为系统需要管理窗口的全屏、无边框、置顶、透明等多种行为,这些行为在不同平台(Windows/X11/Wayland/Embedded)上的支持情况各异,且某些行为之间天然互斥(如 StayOnTop 与 StayOnBottom)。采用分层架构 + 策略模式的核心原因是:将行为的"意图"与"执行"彻底分离,使得应用层代码只描述"想要什么行为",而由底层各层负责"能不能做、怎么冲突解决、最终怎么映射到平台 API"。 ---- - -## 系统架构概览 - -### 设计目标 - -桌面行为系统的核心设计目标是构建一个**可扩展、跨平台、插件化**的窗口行为管理框架。该框架需要: - -1. **抽象统一**:将不同平台的具体行为抽象为统一的行为模型 -2. **策略可插拔**:支持通过插件方式添加新的行为策略 -3. **冲突可控**:提供明确的冲突检测和解决机制 -4. **平台无关**:应用层代码不依赖于特定平台 API - -### 架构原则 - -```cpp -// 核心架构原则 -namespace desktop::architecture { - -// 1. 依赖倒置:高层模块不依赖低层模块,都依赖抽象 -// 2. 开闭原则:对扩展开放,对修改关闭 -// 3. 单一职责:每个组件只负责一个明确的功能 -// 4. 接口隔离:客户端不应该依赖它不需要的接口 - -} // namespace desktop::architecture -```yaml - ---- - -## 分层架构设计 - -### 整体分层视图 - -```text -┌─────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (用户代码 / 业务逻辑层) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Behavior Abstraction │ -│ (行为抽象层 - Domain) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ DesktopBehaviors (QFlags) │ │ -│ │ - 行为查询接口 (IDesktopBehaviorQuery) │ │ -│ │ - 行为修改接口 (IDesktopBehaviorModifier) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Strategy Layer │ -│ (策略层 - Business) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Fullscreen │ │ Frameless │ │ StayOnTop │ ... │ -│ │ Strategy │ │ Strategy │ │ Strategy │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Strategy Factory / Registry │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Qt Integration Layer │ -│ (Qt 集成层 - Implementation) │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Qt WindowFlags / WindowState │ │ -│ │ QWidget / QWindow 行为适配 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Platform Abstraction │ -│ (平台抽象层 - Infrastructure) │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ Windows │ │ X11 │ │ Wayland │ │ Embedded│ ... │ -│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Qt Platform Abstraction (QPA) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ Window Manager / OS │ -│ (窗口管理器 / 操作系统层) │ -└─────────────────────────────────────────────────────────────────┘ -```text - -### 各层职责详解 - -#### 1. Application Layer(应用层) - -**职责**:业务逻辑和用户交互 - -```cpp -// 应用层代码示例 -namespace desktop::app { - -class MainWindow : public QWidget { -public: - void enableFullscreenMode() { - // 直接使用行为抽象,不关心具体实现 - auto behaviors = m_behaviorQuery->behaviors(); - behaviors |= DesktopBehaviorFlag::Fullscreen; - m_behaviorModifier->setBehaviors(behaviors); - } - -private: - IDesktopBehaviorQuery* m_behaviorQuery; - IDesktopBehaviorModifier* m_behaviorModifier; -}; - -} // namespace desktop::app -```text - -**特点**: -- 完全不依赖 Qt 具体类 -- 不包含平台相关代码 -- 易于测试和模拟 - -#### 2. Behavior Abstraction Layer(行为抽象层) - -**职责**:定义行为模型和操作接口 - -```cpp -// DesktopBehaviorAbstraction.h -#pragma once -#include - -namespace desktop::abstraction { - -// 行为标志定义 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - Modal = 1 << 8, - Tool = 1 << 9, -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(DesktopBehaviors) - -// 行为查询接口 -class IDesktopBehaviorQuery { -public: - virtual ~IDesktopBehaviorQuery() = default; - - virtual DesktopBehaviors behaviors() const = 0; - virtual bool hasBehavior(DesktopBehaviorFlag flag) const = 0; - virtual bool hasAnyBehavior(DesktopBehaviors flags) const = 0; - virtual bool hasAllBehaviors(DesktopBehaviors flags) const = 0; -}; - -// 行为修改接口 -class IDesktopBehaviorModifier { -public: - virtual ~IDesktopBehaviorModifier() = default; - - virtual void setBehaviors(DesktopBehaviors flags) = 0; - virtual void addBehaviors(DesktopBehaviors flags) = 0; - virtual void removeBehaviors(DesktopBehaviors flags) = 0; - virtual void clearBehaviors() = 0; -}; - -} // namespace desktop::abstraction -```text - -#### 3. Strategy Layer(策略层) - -**职责**:实现具体的行为策略 - -```cpp -// StrategyLayer.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::strategy { - -// 策略接口 -class IDesktopBehaviorStrategy { -public: - virtual ~IDesktopBehaviorStrategy() = default; - - // Query:查询策略能提供的行为 - virtual DesktopBehaviors query() const = 0; - - // Action:应用策略到目标 - virtual bool apply(IDesktopBehaviorModifier* modifier) = 0; - - // 优先级(用于冲突解决) - virtual int priority() const = 0; - - // 策略名称 - virtual QString name() const = 0; -}; - -// 策略工厂 -class IStrategyFactory { -public: - virtual ~IStrategyFactory() = default; - - virtual IDesktopBehaviorStrategy* create(const QString& strategyName) = 0; - virtual QStringList availableStrategies() const = 0; -}; - -// 策略注册表 -class StrategyRegistry { -public: - static StrategyRegistry& instance(); - - void registerFactory(const QString& name, IStrategyFactory* factory); - IDesktopBehaviorStrategy* createStrategy(const QString& name); - -private: - QMap m_factories; -}; - -} // namespace desktop::strategy -```text - -#### 4. Qt Integration Layer(Qt 集成层) - -**职责**:将行为抽象转换为 Qt API 调用 - -```cpp -// QtIntegrationLayer.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include - -namespace desktop::qtintegration { - -// Qt 行为修改器实现 -class QtBehaviorModifier : public abstraction::IDesktopBehaviorModifier { -public: - explicit QtBehaviorModifier(QWidget* target) - : m_target(target) {} - - void setBehaviors(DesktopBehaviors flags) override { - if (!m_target) return; - - Qt::WindowFlags windowFlags = m_target->windowFlags(); - - // 清除相关标志 - windowFlags &= ~(Qt::FramelessWindowHint | - Qt::WindowStaysOnTopHint | - Qt::WindowStaysOnBottomHint | - Qt::ToolTip | - Qt::SplashScreen); - - // 应用新标志 - if (flags.testFlag(DesktopBehaviorFlag::Frameless)) - windowFlags |= Qt::FramelessWindowHint; - if (flags.testFlag(DesktopBehaviorFlag::StayOnTop)) - windowFlags |= Qt::WindowStaysOnTopHint; - if (flags.testFlag(DesktopBehaviorFlag::StayOnBottom)) - windowFlags |= Qt::WindowStaysOnBottomHint; - if (flags.testFlag(DesktopBehaviorFlag::Tool)) - windowFlags |= Qt::Tool; - if (flags.testFlag(DesktopBehaviorFlag::Splash)) - windowFlags |= Qt::SplashScreen; - - m_target->setWindowFlags(windowFlags); - - // 处理全屏 - if (flags.testFlag(DesktopBehaviorFlag::Fullscreen)) { - m_target->showFullScreen(); - } else if (m_target->isFullScreen()) { - m_target->showNormal(); - } - - // 处理大小调整 - if (!flags.testFlag(DesktopBehaviorFlag::AllowResize)) { - m_target->setFixedSize(m_target->size()); - } - } - - void addBehaviors(DesktopBehaviors flags) override { - auto current = queryCurrent(); - setBehaviors(current | flags); - } - - void removeBehaviors(DesktopBehaviors flags) override { - auto current = queryCurrent(); - setBehaviors(current & ~flags); - } - - void clearBehaviors() override { - setBehaviors(DesktopBehaviorFlag::None); - } - -private: - DesktopBehaviors queryCurrent() const { - DesktopBehaviors result = DesktopBehaviorFlag::None; - if (m_target->isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - if (m_target->windowFlags() & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (m_target->windowFlags() & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (m_target->windowFlags() & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - if (m_target->windowFlags() & Qt::Tool) - result |= DesktopBehaviorFlag::Tool; - return result; - } - - QWidget* m_target; -}; - -// Qt 行为查询器实现 -class QtBehaviorQuery : public abstraction::IDesktopBehaviorQuery { -public: - explicit QtBehaviorQuery(QWidget* target) - : m_target(target) {} - - DesktopBehaviors behaviors() const override { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - if (m_target->isFullScreen()) - result |= DesktopBehaviorFlag::Fullscreen; - - Qt::WindowFlags flags = m_target->windowFlags(); - if (flags & Qt::FramelessWindowHint) - result |= DesktopBehaviorFlag::Frameless; - if (flags & Qt::WindowStaysOnTopHint) - result |= DesktopBehaviorFlag::StayOnTop; - if (flags & Qt::WindowStaysOnBottomHint) - result |= DesktopBehaviorFlag::StayOnBottom; - if (flags & Qt::Tool) - result |= DesktopBehaviorFlag::Tool; - if (flags & Qt::SplashScreen) - result |= DesktopBehaviorFlag::Splash; - - // 推断行为 - if (canResize()) - result |= DesktopBehaviorFlag::AllowResize; - - return result; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return behaviors().testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (behaviors() & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (behaviors() & flags) == flags; - } - -private: - bool canResize() const { - return m_target->minimumSize().isEmpty() && - m_target->maximumSize().isEmpty(); - } - - QWidget* m_target; -}; - -} // namespace desktop::qtintegration -```text - -#### 5. Platform Abstraction Layer(平台抽象层) - -**职责**:处理平台特定的行为差异 - -Qt 本身提供了 **Qt Platform Abstraction (QPA)** 层来处理平台差异。根据 [Qt Platform Abstraction 官方文档](https://doc.qt.io/qt-6/qpa.html): - -> "The Qt Platform Abstraction (QPA) is the main platform abstraction layer in Qt. -> The API can be identified by the QPlatform* class prefix." - -在我们的系统中,可以利用 QPA 来处理平台特定的行为: - -```cpp -// PlatformAbstraction.h -#pragma once -#include - -namespace desktop::platform { - -enum class PlatformType { - Windows, - macOS, - X11, - Wayland, - Embedded, - Unknown -}; - -class PlatformInfo { -public: - static PlatformType currentPlatform() { - QString platformName = QGuiApplication::platformName(); - - if (platformName == "windows") - return PlatformType::Windows; - if (platformName == "cocoa" || platformName == "macos") - return PlatformType::macOS; - if (platformName == "xcb") - return PlatformType::X11; - if (platformName == "wayland") - return PlatformType::Wayland; - if (platformName.startsWith("eglfs") || - platformName == "linuxfb" || - platformName == "offscreen") - return PlatformType::Embedded; - - return PlatformType::Unknown; - } - - static bool supportsAlwaysOnTop(PlatformType platform) { - switch (platform) { - case PlatformType::Windows: - case PlatformType::macOS: - case PlatformType::X11: - return true; - case PlatformType::Wayland: - return false; // Wayland 不支持 - case PlatformType::Embedded: - return false; - default: - return false; - } - } - - static bool supportsFrameless(PlatformType platform) { - // 所有平台都支持无边框 - return true; - } - - static bool supportsClickThrough(PlatformType platform) { - switch (platform) { - case PlatformType::Windows: - return true; - case PlatformType::X11: - return true; - default: - return false; - } - } -}; - -// 平台行为过滤器 -class PlatformBehaviorFilter { -public: - static DesktopBehaviors filter(DesktopBehaviors behaviors) { - PlatformType platform = PlatformInfo::currentPlatform(); - - // 移除不支持的标志 - if (!PlatformInfo::supportsAlwaysOnTop(platform)) { - behaviors &= ~DesktopBehaviorFlag::StayOnTop; - behaviors &= ~DesktopBehaviorFlag::StayOnBottom; - } - - if (!PlatformInfo::supportsClickThrough(platform)) { - behaviors &= ~DesktopBehaviorFlag::ClickThrough; - } - - return behaviors; - } -}; - -} // namespace desktop::platform -```text - -### 层间通信协议 - -```cpp -// LayerCommunication.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include "QtIntegrationLayer.h" -#include "PlatformAbstraction.h" - -namespace desktop::communication { - -// 行为请求 -struct BehaviorRequest { - DesktopBehaviors desired; - DesktopBehaviors current; - QString context; // 请求上下文(如 "user_action", "plugin_request") -}; - -// 行为响应 -struct BehaviorResponse { - bool success; - DesktopBehaviors applied; - QStringList warnings; - QStringList errors; -}; - -// 行为协调器(连接各层) -class BehaviorCoordinator { -public: - BehaviorResponse apply(const BehaviorRequest& request) { - BehaviorResponse response; - - // 1. 平台过滤 - DesktopBehaviors filtered = - platform::PlatformBehaviorFilter::filter(request.desired); - - // 2. 冲突解决 - DesktopBehaviors resolved = - m_conflictResolver.resolve(filtered, request.current); - - // 3. 应用到 Qt - response.success = m_qtModifier->setBehaviors(resolved); - response.applied = resolved; - - // 4. 检查警告 - if (filtered != request.desired) { - response.warnings << "Some behaviors were filtered due to platform limitations"; - } - - return response; - } - -private: - qtintegration::QtBehaviorModifier* m_qtModifier; - ConflictResolver m_conflictResolver; -}; - -} // namespace desktop::communication -```yaml - ---- - -## 行为流转流程 - -### 完整流程图 - -```text -┌─────────────────────────────────────────────────────────────────┐ -│ 行为变更请求 │ -│ (User / Plugin / System) │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Strategy 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 调用相关策略的 query() 获取行为 │ │ -│ │ - 策略可能来自多个来源 (User, Plugin, System) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Merge 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 合并来自不同策略的行为请求 │ │ -│ │ - 使用位或运算: final = s1 | s2 | s3 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Resolve 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 检测行为冲突 │ │ -│ │ - 应用优先级规则 │ │ -│ │ - 执行冲突解决策略 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Filter 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 平台能力过滤 │ │ -│ │ - 移除当前平台不支持的行为 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Action 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 调用策略的 apply() 方法 │ │ -│ │ - 将行为转换为 Qt API 调用 │ │ -│ │ - 更新窗口状态 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. Validate 阶段 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ - 验证行为是否正确应用 │ │ -│ │ - 检测实际状态与期望状态的差异 │ │ -│ │ - 记录任何不一致问题 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 行为变更完成 │ -│ (通知观察者 / 触发事件) │ -└─────────────────────────────────────────────────────────────────┘ -```text - -### 流程代码实现 - -```cpp -// BehaviorFlow.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include "StrategyLayer.h" -#include "PlatformAbstraction.h" -#include - -namespace desktop::flow { - -// 行为变更请求 -struct BehaviorChangeRequest { - DesktopBehaviors desired; - QString source; // "user", "plugin_xxx", "system" - int priority; // 0-100, 数值越大优先级越高 -}; - -// 行为变更结果 -struct BehaviorChangeResult { - bool success; - DesktopBehaviors final; - DesktopBehaviors applied; - QStringList warnings; - QStringList errors; -}; - -// 行为流程引擎 -class BehaviorFlowEngine { -public: - BehaviorChangeResult apply(const QVector& requests) { - BehaviorChangeResult result; - - // 阶段 1: Strategy - 收集所有策略的请求 - QVector strategyRequests = collectStrategyRequests(requests); - - // 阶段 2: Merge - 合并请求 - DesktopBehaviors merged = mergeRequests(strategyRequests); - - // 获取当前行为 - DesktopBehaviors current = queryCurrentBehaviors(); - - // 阶段 3: Resolve - 解决冲突 - DesktopBehaviors resolved = m_conflictResolver.resolve(merged, current); - - // 阶段 4: Filter - 平台过滤 - DesktopBehaviors filtered = platform::PlatformBehaviorFilter::filter(resolved); - - if (filtered != resolved) { - result.warnings << "Some behaviors were filtered due to platform limitations"; - } - - // 阶段 5: Action - 应用行为 - if (m_modifier) { - m_modifier->setBehaviors(filtered); - } - - // 阶段 6: Validate - 验证结果 - DesktopBehaviors actual = queryCurrentBehaviors(); - result.success = (actual == filtered); - result.final = actual; - result.applied = filtered; - - if (!result.success) { - result.errors << "Applied behaviors do not match expected"; - } - - return result; - } - - void setModifier(IDesktopBehaviorModifier* modifier) { - m_modifier = modifier; - } - - void setQuery(IDesktopBehaviorQuery* query) { - m_query = m_query; - } - -private: - struct StrategyRequest { - DesktopBehaviors behaviors; - QString source; - int priority; - }; - - QVector collectStrategyRequests( - const QVector& requests) { - - QVector strategyRequests; - for (const auto& req : requests) { - strategyRequests.append({ - req.desired, - req.source, - req.priority - }); - } - - // 添加系统策略 - strategyRequests.append({ - m_systemStrategy->query(), - "system", - 50 - }); - - return strategyRequests; - } - - DesktopBehaviors mergeRequests(const QVector& requests) { - DesktopBehaviors result = DesktopBehaviorFlag::None; - - // 按优先级排序 - auto sorted = requests; - std::sort(sorted.begin(), sorted.end(), - [](const StrategyRequest& a, const StrategyRequest& b) { - return a.priority > b.priority; - }); - - // 合并行为(高优先级的覆盖低优先级的) - for (const auto& req : sorted) { - result |= req.behaviors; - } - - return result; - } - - DesktopBehaviors queryCurrentBehaviors() const { - return m_query ? m_query->behaviors() : DesktopBehaviorFlag::None; - } - - IDesktopBehaviorModifier* m_modifier = nullptr; - IDesktopBehaviorQuery* m_query = nullptr; - IDesktopBehaviorStrategy* m_systemStrategy = nullptr; - ConflictResolver m_conflictResolver; -}; - -} // namespace desktop::flow -```text - -### 流程监控与调试 - -```cpp -// FlowMonitoring.h -#pragma once -#include "BehaviorFlow.h" - -namespace desktop::flow { - -// 流程跟踪器 -class FlowTracer { -public: - void traceRequest(const BehaviorChangeRequest& request) { - qDebug() << "[FlowTracer] Request from" << request.source - << ":" << formatBehaviors(request.desired); - } - - void traceMerge(const DesktopBehaviors& merged) { - qDebug() << "[FlowTracer] Merged:" << formatBehaviors(merged); - } - - void traceConflict(const DesktopBehaviors& conflict) { - qDebug() << "[FlowTracer] Conflict detected:" << formatBehaviors(conflict); - } - - void traceResolution(const DesktopBehaviors& resolved, - const DesktopBehaviors& removed) { - qDebug() << "[FlowTracer] Resolved to:" << formatBehaviors(resolved) - << "Removed:" << formatBehaviors(removed); - } - - void traceFilter(const DesktopBehaviors& before, - const DesktopBehaviors& after) { - qDebug() << "[FlowTracer] Filtered:" << formatBehaviors(before) - << "->" << formatBehaviors(after); - } - - void traceResult(const BehaviorChangeResult& result) { - qDebug() << "[FlowTracer] Result: success=" << result.success; - if (!result.warnings.isEmpty()) { - qDebug() << "[FlowTracer] Warnings:" << result.warnings; - } - if (!result.errors.isEmpty()) { - qDebug() << "[FlowTracer] Errors:" << result.errors; - } - } - -private: - QString formatBehaviors(DesktopBehaviors behaviors) { - QStringList flags; - if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) - flags << "Fullscreen"; - if (behaviors.testFlag(DesktopBehaviorFlag::Frameless)) - flags << "Frameless"; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnTop)) - flags << "StayOnTop"; - if (behaviors.testFlag(DesktopBehaviorFlag::StayOnBottom)) - flags << "StayOnBottom"; - if (behaviors.testFlag(DesktopBehaviorFlag::AllowResize)) - flags << "AllowResize"; - return flags.join("|"); - } -}; - -} // namespace desktop::flow -```yaml - ---- - -## 冲突解决机制 - -### 冲突类型定义 - -```cpp -// ConflictResolution.h -#pragma once -#include "DesktopBehaviorAbstraction.h" -#include - -namespace desktop::conflict { - -// 冲突类型 -enum class ConflictType { - MutuallyExclusive, // 互斥冲突(如 StayOnTop vs StayOnBottom) - Incompatible, // 不兼容冲突(如 Fullscreen vs AllowResize) - PlatformUnsupported, // 平台不支持 - PriorityOverride, // 优先级覆盖 -}; - -// 冲突定义 -struct ConflictDefinition { - DesktopBehaviorFlag flag1; - DesktopBehaviorFlag flag2; - ConflictType type; - QString description; - DesktopBehaviorFlag preferred; // 优先保留的标志 -}; - -// 冲突规则 -class ConflictRules { -public: - static QVector defaultRules() { - return { - // 互斥冲突 - { - DesktopBehaviorFlag::StayOnTop, - DesktopBehaviorFlag::StayOnBottom, - ConflictType::MutuallyExclusive, - "Window cannot be both on top and on bottom", - DesktopBehaviorFlag::StayOnTop // 优先保留 StayOnTop - }, - // 不兼容冲突 - { - DesktopBehaviorFlag::Fullscreen, - DesktopBehaviorFlag::AllowResize, - ConflictType::Incompatible, - "Fullscreen window should not be resizable", - DesktopBehaviorFlag::Fullscreen - }, - { - DesktopBehaviorFlag::Modal, - DesktopBehaviorFlag::Tool, - ConflictType::Incompatible, - "Modal window cannot be a tool window", - DesktopBehaviorFlag::Modal - }, - }; - } -}; - -// 解决策略 -enum class ResolutionStrategy { - Fail, // 失败,不应用任何行为 - Warning, // 警告,自动解决冲突 - AutoResolve, // 自动解决(使用优先级) - Prioritize, // 使用预定义优先级 - RemoveBoth, // 移除冲突的两个标志 -}; - -// 冲突解决器 -class ConflictResolver { -public: - ConflictResolver() - : m_resolutionStrategy(ResolutionStrategy::Prioritize) { - m_rules = ConflictRules::defaultRules(); - } - - void setResolutionStrategy(ResolutionStrategy strategy) { - m_resolutionStrategy = strategy; - } - - DesktopBehaviors resolve(const DesktopBehaviors& desired, - const DesktopBehaviors& current) { - DesktopBehaviors result = desired; - QVector conflicts = detectConflicts(desired); - - if (!conflicts.isEmpty()) { - switch (m_resolutionStrategy) { - case ResolutionStrategy::Fail: - return current; // 不做任何修改 - - case ResolutionStrategy::Warning: - emitWarning(conflicts); - result = autoResolve(result, conflicts); - break; - - case ResolutionStrategy::AutoResolve: - result = autoResolve(result, conflicts); - break; - - case ResolutionStrategy::Prioritize: - result = prioritizeResolve(result, conflicts); - break; - - case ResolutionStrategy::RemoveBoth: - result = removeBothResolve(result, conflicts); - break; - } - } +行为流转采用六阶段管线(Strategy -> Query -> Merge -> Resolve -> Filter -> Action -> Validate),每一阶段职责单一、可独立测试。这种管线式设计让冲突检测和平台过滤成为流程的显式步骤而非事后补救,确保行为变更的结果可预测。 - return result; - } - - QVector detectConflicts(const DesktopBehaviors& behaviors) { - QVector conflicts; - - for (const auto& rule : m_rules) { - if (behaviors.testFlag(rule.flag1) && - behaviors.testFlag(rule.flag2)) { - conflicts.append({ - rule.flag1, - rule.flag2, - rule.type, - rule.description - }); - } - } - - return conflicts; - } - -private: - struct Conflict { - DesktopBehaviorFlag flag1; - DesktopBehaviorFlag flag2; - ConflictType type; - QString description; - }; - - void emitWarning(const QVector& conflicts) { - for (const auto& conflict : conflicts) { - qWarning() << "[ConflictResolver]" << conflict.description; - } - } - - DesktopBehaviors autoResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - for (const auto& conflict : conflicts) { - // 查找对应的规则 - for (const auto& rule : m_rules) { - if (conflict.flag1 == rule.flag1 && - conflict.flag2 == rule.flag2) { - // 移除非优先标志 - result &= ~rule.preferred; - result |= rule.preferred; - break; - } - } - } - - return result; - } - - DesktopBehaviors prioritizeResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - // 使用预定义优先级 - QMap priorities = { - {DesktopBehaviorFlag::Fullscreen, 100}, - {DesktopBehaviorFlag::Modal, 90}, - {DesktopBehaviorFlag::StayOnTop, 80}, - {DesktopBehaviorFlag::Frameless, 70}, - {DesktopBehaviorFlag::StayOnBottom, 60}, - {DesktopBehaviorFlag::Tool, 50}, - {DesktopBehaviorFlag::AllowResize, 40}, - {DesktopBehaviorFlag::AvoidSystemUI, 30}, - {DesktopBehaviorFlag::Transparent, 20}, - {DesktopBehaviorFlag::ClickThrough, 10}, - }; - - for (const auto& conflict : conflicts) { - int priority1 = priorities.value(conflict.flag1, 0); - int priority2 = priorities.value(conflict.flag2, 0); - - if (priority1 > priority2) { - result &= ~conflict.flag2; // 移除低优先级的 - } else { - result &= ~conflict.flag1; - } - } - - return result; - } - - DesktopBehaviors removeBothResolve(const DesktopBehaviors& behaviors, - const QVector& conflicts) { - DesktopBehaviors result = behaviors; - - for (const auto& conflict : conflicts) { - result &= ~conflict.flag1; - result &= ~conflict.flag2; - } - - return result; - } - - ResolutionStrategy m_resolutionStrategy; - QVector m_rules; -}; - -// 冲突解决结果 -struct ResolutionResult { - bool hasConflicts; - DesktopBehaviors resolved; - QStringList warnings; - QVector conflicts; -}; - -} // namespace desktop::conflict -```text - -### 优先级系统 - -```cpp -// PrioritySystem.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::priority { - -// 行为优先级 -enum class BehaviorPriority { - Critical = 100, // 关键行为(如 Modal) - High = 80, // 高优先级(如 StayOnTop) - Medium = 50, // 中优先级 - Low = 20, // 低优先级 - Optional = 0 // 可选行为 -}; - -// 优先级管理器 -class PriorityManager { -public: - static BehaviorPriority getPriority(DesktopBehaviorFlag flag) { - static QMap priorities = { - {DesktopBehaviorFlag::Modal, BehaviorPriority::Critical}, - {DesktopBehaviorFlag::Fullscreen, BehaviorPriority::High}, - {DesktopBehaviorFlag::StayOnTop, BehaviorPriority::High}, - {DesktopBehaviorFlag::Frameless, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::StayOnBottom, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::Tool, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::Splash, BehaviorPriority::Medium}, - {DesktopBehaviorFlag::AllowResize, BehaviorPriority::Low}, - {DesktopBehaviorFlag::AvoidSystemUI, BehaviorPriority::Low}, - {DesktopBehaviorFlag::Transparent, BehaviorPriority::Optional}, - {DesktopBehaviorFlag::ClickThrough, BehaviorPriority::Optional}, - }; - return priorities.value(flag, BehaviorPriority::Optional); - } - - // 按优先级排序行为 - static QVector sortByPriority( - const QVector& flags) { - - QVector sorted = flags; - std::sort(sorted.begin(), sorted.end(), - [](DesktopBehaviorFlag a, DesktopBehaviorFlag b) { - return static_cast(getPriority(a)) > - static_cast(getPriority(b)); - }); - return sorted; - } -}; - -} // namespace desktop::priority -```yaml - ---- - -## 插件系统集成 - -### Qt 插件架构概述 - -Qt 提供了强大的插件系统,允许应用程序在运行时动态加载扩展。根据 [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html): - -> "Writing a plugin that extends Qt itself is achieved by subclassing the appropriate plugin base class, implementing a few functions, and adding a macro." - -### 插件接口定义 - -```cpp -// DesktopBehaviorPlugin.h -#pragma once -#include -#include "StrategyLayer.h" - -namespace desktop::plugin { - -// 插件接口 -class IDesktopBehaviorPlugin { -public: - virtual ~IDesktopBehaviorPlugin() = default; - - // 插件信息 - virtual QString pluginName() const = 0; - virtual QString pluginVersion() const = 0; - virtual QString pluginDescription() const = 0; - - // 策略提供 - virtual QVector providedStrategies() const = 0; - virtual IDesktopBehaviorStrategy* createStrategy( - const QString& strategyName) = 0; - - // 初始化/清理 - virtual bool initialize() = 0; - virtual void cleanup() = 0; -}; - -// Qt 插件接口(用于 QPluginLoader) -#define DesktopBehaviorPlugin_iid "com.desktop.behavior.plugin/1.0" - -} // namespace desktop::plugin - -Q_DECLARE_INTERFACE(desktop::plugin::IDesktopBehaviorPlugin, - desktop::plugin::DesktopBehaviorPlugin_iid) -```text - -### 插件管理器 - -```cpp -// PluginManager.h -#pragma once -#include "DesktopBehaviorPlugin.h" -#include -#include - -namespace desktop::plugin { - -// 插件信息 -struct PluginInfo { - QString name; - QString version; - QString description; - QString filePath; - IDesktopBehaviorPlugin* plugin; - bool isLoaded; -}; - -// 插件管理器 -class PluginManager : public QObject { - Q_OBJECT - -public: - static PluginManager& instance(); - - // 加载插件 - bool loadPlugin(const QString& pluginPath); - - // 加载插件目录 - int loadPluginsFromDirectory(const QString& directory); - - // 卸载插件 - bool unloadPlugin(const QString& pluginName); - - // 获取所有插件 - QVector loadedPlugins() const; - - // 创建策略 - IDesktopBehaviorStrategy* createStrategy( - const QString& pluginName, - const QString& strategyName); - - // 获取所有可用策略 - QVector availableStrategies() const; - -signals: - void pluginLoaded(const QString& pluginName); - void pluginUnloaded(const QString& pluginName); - void pluginLoadFailed(const QString& pluginName, const QString& error); - -private: - PluginManager() = default; - - QMap m_plugins; -}; - -// 单例实现 -PluginManager& PluginManager::instance() { - static PluginManager manager; - return manager; -} - -} // namespace desktop::plugin -```text - -### 插件实现示例 - -```cpp -// ExamplePlugin.h -#pragma once -#include "DesktopBehaviorPlugin.h" - -namespace desktop::plugin::example { - -// 示例策略:全屏策略 -class ExampleFullscreenStrategy : public strategy::IDesktopBehaviorStrategy { -public: - DesktopBehaviors query() const override { - return DesktopBehaviorFlag::Fullscreen; - } - - bool apply(IDesktopBehaviorModifier* modifier) override { - if (!modifier) return false; - - DesktopBehaviors behaviors = modifier->behaviors(); - behaviors |= DesktopBehaviorFlag::Fullscreen; - behaviors &= ~DesktopBehaviorFlag::AllowResize; // 全屏时不允许调整大小 - - modifier->setBehaviors(behaviors); - return true; - } - - int priority() const override { - return 90; - } - - QString name() const override { - return "ExampleFullscreen"; - } -}; - -// 示例插件 -class ExamplePlugin : public QObject, - public IDesktopBehaviorPlugin { - Q_OBJECT - Q_PLUGIN_METADATA(IID DesktopBehaviorPlugin_iid FILE "example.json") - Q_INTERFACES(desktop::plugin::IDesktopBehaviorPlugin) - -public: - QString pluginName() const override { - return "ExamplePlugin"; - } - - QString pluginVersion() const override { - return "1.0.0"; - } - - QString pluginDescription() const override { - return "Example behavior plugin with fullscreen strategy"; - } - - QVector providedStrategies() const override { - return {"ExampleFullscreen"}; - } - - IDesktopBehaviorStrategy* createStrategy( - const QString& strategyName) override { - - if (strategyName == "ExampleFullscreen") { - return new ExampleFullscreenStrategy(); - } - return nullptr; - } - - bool initialize() override { - qDebug() << "ExamplePlugin initialized"; - return true; - } - - void cleanup() override { - qDebug() << "ExamplePlugin cleaned up"; - } -}; - -} // namespace desktop::plugin::example -```text - -### 插件元数据文件 - -```json -// example.json -{ - "Keys": ["ExampleFullscreen"], - "ClassName": "desktop::plugin::example::ExamplePlugin", - "IID": "com.desktop.behavior.plugin/1.0", - "MetaData": { - "Author": "Desktop Team", - "Date": "2025-01-01", - "Description": "Example behavior plugin", - "Version": "1.0.0", - "MinQtVersion": "6.5.0", - "RequiredFeatures": [] - } -} -```text - -### 动态加载策略 - -根据 [QPluginLoader Class | Qt Core | Qt 6.10.2](https://doc.qt.io/qt-6/qpluginloader.html): - -```cpp -// DynamicStrategyLoader.h -#pragma once -#include "PluginManager.h" - -namespace desktop::plugin { - -// 动态策略加载器 -class DynamicStrategyLoader { -public: - // 从插件加载策略 - static IDesktopBehaviorStrategy* loadStrategy( - const QString& pluginName, - const QString& strategyName) { - - PluginManager& manager = PluginManager::instance(); - return manager.createStrategy(pluginName, strategyName); - } - - // 按能力查找策略 - static QVector findStrategiesByCapability( - DesktopBehaviorFlag capability) { - - QVector result; - PluginManager& manager = PluginManager::instance(); - - for (const auto& pluginInfo : manager.loadedPlugins()) { - if (!pluginInfo.isLoaded) continue; - - for (const auto& strategyName : pluginInfo.plugin->providedStrategies()) { - auto* strategy = pluginInfo.plugin->createStrategy(strategyName); - if (strategy && strategy->query().testFlag(capability)) { - result.append(strategy); - } - } - } - - return result; - } -}; - -} // namespace desktop::plugin -```yaml - ---- - -## 未来扩展方向 - -### 新增行为特性 - -```cpp -// FutureBehaviors.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::future { - -// 未来可能的行为标志 -enum class FutureBehaviorFlag { - // 现有行为... - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - Transparent = 1 << 6, - ClickThrough = 1 << 7, - - // 新增行为 - AlwaysOnTop = 1 << 10, ///< 始终置顶(与 StayOnTop 类似但语义不同) - AlwaysOnBottom = 1 << 11, ///< 始终置底 - VirtualDesktop = 1 << 12, ///< 虚拟桌面支持 - SkipTaskbar = 1 << 13, ///< 不显示在任务栏 - SkipPager = 1 << 14, ///< 不显示在页面切换器 - SkipSwitcher = 1 << 15, ///< 不显示在窗口切换器 - Above = 1 << 16, ///< 位于特定窗口之上 - Below = 1 << 17, ///< 位于特定窗口之下 - DemandsAttention = 1 << 18, ///< 需要注意(闪烁等) - Focused = 1 << 19, ///< 获取焦点 - AcceptFocus = 1 << 20, ///< 接受焦点 - FocusOnButtonClick = 1 << 21, ///< 点击时获取焦点 - NoFocus = 1 << 22, ///< 不获取焦点 - GroupLeader = 1 << 23, ///< 组领导者 - Independent = 1 << 24, ///< 独立窗口 - BypassWindowManager = 1 << 25, ///< 绕过窗口管理器 - Maximized = 1 << 26, ///< 最大化 - Minimized = 1 << 27, ///< 最小化 - Active = 1 << 28, ///< 活动窗口 - Dock = 1 << 29, ///< 停靠窗口 - Desktop = 1 << 30, ///< 桌面窗口类型 -}; - -Q_DECLARE_FLAGS(FutureBehaviors, FutureBehaviorFlag) -Q_DECLARE_OPERATORS_FOR_FLAGS(FutureBehaviors) - -// 扩展的行为接口 -class IFutureDesktopBehaviorQuery { -public: - virtual ~IFutureDesktopBehaviorQuery() = default; - - virtual FutureBehaviors futureBehaviors() const = 0; - virtual bool hasFutureBehavior(FutureBehaviorFlag flag) const = 0; - - // 虚拟桌面相关 - virtual int currentVirtualDesktop() const = 0; - virtual void setVirtualDesktop(int desktop) = 0; - virtual QVector availableVirtualDesktops() const = 0; - - // 窗口层级相关 - virtual void setWindowLevel(WindowLevel level) = 0; - virtual WindowLevel windowLevel() const = 0; - - // 注意力相关 - virtual void setDemandsAttention(bool demand) = 0; - virtual bool demandsAttention() const = 0; -}; - -// 窗口层级定义 -enum class WindowLevel { - Normal, - Above, - Below, - TopMost, - BottomMost -}; - -} // namespace desktop::future -```text - -### 平台特定扩展 - -```cpp -// PlatformExtensions.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::platform { - -// Windows 特定扩展 -#ifdef Q_OS_WIN -struct WindowsSpecificBehaviors { - bool appWindow = false; // 应用程序窗口 - bool toolWindow = false; // 工具窗口 - bool noActivate = false; // 不激活 - bool topMost = false; // 最顶层 -}; -#endif - -// macOS 特定扩展 -#ifdef Q_OS_MACOS -struct MacOSSpecificBehaviors { - bool floatingPanel = false; // 浮动面板 - bool hudWindow = false; // HUD 窗口 - bool utilityWindow = false; // 实用工具窗口 - bool docModal = false; // 文档模态 -}; -#endif - -// Linux 特定扩展 -#ifdef Q_OS_LINUX -struct LinuxSpecificBehaviors { - bool dockWindow = false; // 停靠窗口 - bool desktopWidget = false; // 桌面部件 - bool notification = false; // 通知窗口 - bool comboBoxPopup = false; // 组合框弹出 - bool dndPopup = false; // 拖放弹出 - bool tooltip = false; // 工具提示 -}; - -// X11 特定 -struct X11SpecificBehaviors { - bool bypassWindowManager = false; // 绕过窗口管理器 - bool x11BypassWM = false; // X11 绕过 WM - uint32_t x11WindowType = 0; // X11 窗口类型 -}; - -// Wayland 特定 -struct WaylandSpecificBehaviors { - bool layerShell = false; // 层级外壳 - bool xdgShell = false; // XDG 外壳 - bool subsurface = false; // 子表面 -}; -#endif - -} // namespace desktop::platform -```yaml - ---- - -## 最佳实践总结 - -### 架构设计原则 - -1. **单一职责原则(SRP)** - - 每个类只负责一个明确的功能 - - Strategy 只负责单一行为的实现 - - Coordinator 只负责流程协调 - -2. **开闭原则(OCP)** - - 对扩展开放:通过插件添加新策略 - - 对修改关闭:核心代码不需要修改 - -3. **依赖倒置原则(DIP)** - - 高层模块不依赖低层模块 - - 都依赖于抽象接口 - -4. **接口隔离原则(ISP)** - - 接口细粒度,客户端不依赖不需要的方法 - - Query 和 Action 分离 - -### 性能优化建议 - -```cpp -// PerformanceOptimizations.h -#pragma once -#include "BehaviorFlow.h" - -namespace desktop::performance { - -// 行为缓存 -class BehaviorCache { -public: - void cache(const QString& key, const DesktopBehaviors& behaviors) { - m_cache[key] = behaviors; - } - - DesktopBehaviors get(const QString& key) const { - return m_cache.value(key, DesktopBehaviorFlag::None); - } - - bool contains(const QString& key) const { - return m_cache.contains(key); - } - - void invalidate(const QString& key) { - m_cache.remove(key); - } - - void clear() { - m_cache.clear(); - } - -private: - QMap m_cache; -}; - -// 批量操作优化 -class BatchBehaviorProcessor { -public: - void addRequest(const BehaviorChangeRequest& request) { - m_requests.append(request); - } - - QVector processAll() { - QVector results; - - // 批量合并请求 - DesktopBehaviors merged = mergeBatchRequests(m_requests); - - // 一次性应用 - BehaviorChangeRequest combined; - combined.desired = merged; - combined.source = "batch"; - combined.priority = 0; - - BehaviorChangeResult result = m_engine.apply({combined}); - results.append(result); - - m_requests.clear(); - return results; - } - -private: - QVector m_requests; - BehaviorFlowEngine m_engine; - - DesktopBehaviors mergeBatchRequests( - const QVector& requests) { - - DesktopBehaviors result = DesktopBehaviorFlag::None; - for (const auto& req : requests) { - result |= req.desired; - } - return result; - } -}; - -} // namespace desktop::performance -```text - -### 测试策略 - -```cpp -// TestingSupport.h -#pragma once -#include "DesktopBehaviorAbstraction.h" - -namespace desktop::testing { - -// Mock 查询器 -class MockBehaviorQuery : public IDesktopBehaviorQuery { -public: - void setBehaviors(DesktopBehaviors behaviors) { - m_behaviors = behaviors; - } - - DesktopBehaviors behaviors() const override { - return m_behaviors; - } - - bool hasBehavior(DesktopBehaviorFlag flag) const override { - return m_behaviors.testFlag(flag); - } - - bool hasAnyBehavior(DesktopBehaviors flags) const override { - return (m_behaviors & flags) != DesktopBehaviorFlag::None; - } - - bool hasAllBehaviors(DesktopBehaviors flags) const override { - return (m_behaviors & flags) == flags; - } - -private: - DesktopBehaviors m_behaviors = DesktopBehaviorFlag::None; -}; - -// Mock 修改器 -class MockBehaviorModifier : public IDesktopBehaviorModifier { -public: - DesktopBehaviors lastSetBehaviors() const { - return m_lastBehaviors; - } - - int setBehaviorsCallCount() const { - return m_callCount; - } - - void setBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors = flags; - m_callCount++; - } - - void addBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors |= flags; - m_callCount++; - } - - void removeBehaviors(DesktopBehaviors flags) override { - m_lastBehaviors &= ~flags; - m_callCount++; - } - - void clearBehaviors() override { - m_lastBehaviors = DesktopBehaviorFlag::None; - m_callCount++; - } - -private: - DesktopBehaviors m_lastBehaviors = DesktopBehaviorFlag::None; - int m_callCount = 0; -}; - -} // namespace desktop::testing -```yaml - ---- - -## 参考资源 - -### Qt 官方文档 - -- [Qt Platform Abstraction | Platform Integration | Qt 6.11.0](https://doc.qt.io/qt-6/qpa.html) -- [QPluginLoader Class | Qt Core | Qt 6.10.2](https://doc.qt.io/qt-6/qpluginloader.html) -- [How to Create Qt Plugins - Qt Documentation](https://doc.qt.io/qt-6/plugins-howto.html) -- [QWidget Class | Qt Widgets | Qt 6.11.0](https://doc.qt.io/qt-6/qwidget.html) -- [Wayland and Qt | Qt 6.11](https://doc.qt.io/qt-6/wayland-and-qt.html) - -### 架构设计参考 - -- [Layered Architecture and Abstraction Layers | by Patryk Rogala](https://medium.com/@patrykrogedu/layered-architecture-and-abstraction-layers-167438dd1a8b) -- [Must-Know Software Architecture Patterns - ByteByteGo Newsletter](https://blog.bytebytego.com/p/must-know-software-architecture-patterns) -- [14 software architecture design patterns to know - Red Hat](https://www.redhat.com/en/blog/14-software-architecture-patterns) -- [Abstraction layer - Wikipedia](https://en.wikipedia.org/wiki/wiki/Abstraction_layer) - -### 插件开发参考 - -- [Adapting Embedded Devices with Qt 6 Plugins | ICS](https://www.ics.com/blog/adapting-embedded-devices-qt-6-plugins-coffee-machine-case-study) -- [Loading and initialising a Qt plug-in dynamically - Qt Wiki](https://qt.shoutwiki.com/wiki/Loading_and_initialising_a_Qt_plug-in_dynamically) - ---- +插件系统基于 Qt QPluginLoader,策略以动态库形式加载。`IDesktopBehaviorStrategy` 接口让每个插件只暴露 `query()` / `apply()` / `priority()` 三个方法,通过 `StrategyRegistry` 统一注册,避免了插件与核心框架的紧耦合。 -## 总结 +## 关键决策 -本文档详细介绍了桌面行为系统的完整架构设计,从分层架构到行为流转流程,从冲突解决机制到插件系统集成。这套架构的核心价值在于: +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 五层架构(Application / Behavior Abstraction / Strategy / Qt Integration / Platform Abstraction) | 每层职责明确,依赖方向单一,便于跨平台扩展 | 三层架构(将 Qt 集成与平台抽象合并):平台差异处理会侵入策略层 | +| 行为以 `QFlags` 表示 | 位运算天然支持行为的合并与冲突检测,性能优于 set/map | `std::unordered_set`:每次查询/合并需要遍历,且无法用位运算做互斥检测 | +| 六阶段管线式流转 | 冲突解决和平台过滤成为显式步骤,结果可预测 | 回调链/观察者模式:行为变更的副作用隐式传播,难以追踪和调试 | +| 冲突解决默认使用 `Prioritize` 策略(预定义优先级表) | 自动解决冲突且行为可预测,优先级表可配置 | `Fail` 策略(遇到冲突直接失败):用户体验差;`RemoveBoth`:丢失合法行为 | +| Query/Modifier 接口分离(ISP) | 只读操作和写操作解耦,便于权限控制和 mock 测试 | 统一 `IDesktopBehavior` 接口:客户端暴露了不需要的修改能力 | +| 插件通过 `QPluginLoader` + `Q_PLUGIN_METADATA` 动态加载 | 支持运行时加载/卸载策略,符合开闭原则 | 静态注册策略:每次新增策略需要重新编译核心框架 | +| 平台能力通过 `PlatformBehaviorFilter` 在管线中显式过滤 | 行为请求先被平台能力裁剪再应用,避免调用不支持的平台 API | 各策略内部判断平台:策略与平台耦合,违反单一职责 | -1. **可扩展性**:通过插件系统轻松添加新的行为策略 -2. **跨平台性**:通过平台抽象层处理不同平台的差异 -3. **可维护性**:清晰的分层架构使代码易于理解和维护 -4. **可测试性**:依赖抽象接口便于编写单元测试 +## 当前状态 -希望这套架构能够为您的桌面应用开发提供有力支持! +架构设计已完成,部分接口已实现。核心接口定义位于 `desktop/` 层,策略注册表与冲突解决器为后续实现重点。 diff --git a/document/notes/05-Logger-Singleton-Link-Architecture.md b/document/notes/05-Logger-Singleton-Link-Architecture.md index 4bb6c2ee0..3ad53f874 100644 --- a/document/notes/05-Logger-Singleton-Link-Architecture.md +++ b/document/notes/05-Logger-Singleton-Link-Architecture.md @@ -1,355 +1,28 @@ --- title: Logger 单实例链接架构 -description: 本文档记录 CFDesktop 项目中 (日志系统)的单实例保证方案,包括 CMake 链接策略、 +description: 通过 INTERFACE 头文件库和 --whole-archive 保证 cflogger 单例在 DLL 边界内唯一 --- -# Logger 单实例链接架构 +# Logger 单实例链接架构 -- 设计意图 -本文档记录 CFDesktop 项目中 `cflogger`(日志系统)的单实例保证方案,包括 CMake 链接策略、`INTERFACE` 头文件库的引入、以及 DLL 导出/导入机制的设计决策。 +## 为什么选择这种方案 ---- - -## 1. 问题背景 - -### 1.1 项目架构 - -CFDesktop 采用"多静态库 → 单一共享库"架构: - -```text -多个 STATIC library (cflogger, cfbase, CFDesktopMain, CFDesktopUi, ...) - ↓ --whole-archive -CFDesktop_shared (SHARED / DLL) - ↓ -CFDesktop.exe -```bash - -所有静态库通过 `--whole-archive` 合并进一个 DLL,EXE 只链接这个 DLL。 - -### 1.2 Logger 单例问题 - -`cflogger` 内部使用单例模式(`Logger::instance()`)。如果同一份 `.cpp` 被编译进多个模块,会产生**多个独立的单例实例**,导致: - -- 日志输出丢失(模块 A 初始化的 Sink,模块 B 看不到) -- 全局状态不一致(日志级别、格式器各自独立) -- 静态初始化顺序问题(Static Initialization Order Fiasco) - -### 1.3 原始问题:cflogger 被到处 PUBLIC 链接 - -修改前,`cflogger` 被多个模块通过 `PUBLIC` 关键字链接: - -| 目标 | 链接方式 | 问题 | -|------|---------|------| -| `CFDesktopMain` | PRIVATE | 不传播,OK | -| `CFDesktopLog` | **PUBLIC** | 向所有消费者传播 | -| `CFDesktopEarlySession` | **PUBLIC** | 向所有消费者传播 | -| `CFDesktopPathSettings` | **PUBLIC** | 向所有消费者传播 | -| `cf_desktop_ui_platform` | **PUBLIC** | 向所有消费者传播 | -| `cf_desktop_ui_widget_init_session` | **PUBLIC** | 向所有消费者传播 | - -`PUBLIC` 链接会在编译期将 `cflogger` 的 include 目录和链接需求传播给所有下游消费者,造成不必要的耦合。虽然在当前 `--whole-archive` 方案下不会产生多实例(因为所有静态库最终打包进同一个 DLL),但这种传播增加了维护复杂度,且容易在架构调整时引入问题。 - ---- - -## 2. 解决方案:INTERFACE 头文件库 - -### 2.1 核心思想 - -引入一个 `cflogger_headers`(INTERFACE library),让 DLL 内部的模块**只拿头文件**,不链接静态库实体。唯一实际链接 `cflogger` 静态库的地方是 `CFDesktop_shared`(通过 `--whole-archive`)和 `CFDesktopMain`(PRIVATE,因为直接使用了内部类)。 - -### 2.2 实现代码 - -在 `desktop/base/logger/CMakeLists.txt` 中新增: - -```cmake -# Header-only interface for internal modules that only need cflog.h -# Modules inside CFDesktop_shared should use this instead of linking cflogger -# to avoid duplicating the static lib symbols — only CFDesktop_shared links the real library. -add_library(cflogger_headers INTERFACE) -target_include_directories(cflogger_headers INTERFACE - $ -) -target_link_libraries(cflogger_headers INTERFACE cfbase) -```cmake - -### 2.3 为什么需要 `cflogger_headers` 而不是直接用 `cflogger` - -| 方式 | 效果 | -|------|------| -| `target_link_libraries(X PUBLIC cflogger)` | X 的消费者也会链接 `libcflogger.a`(传播) | -| `target_link_libraries(X PRIVATE cflogger)` | 只有 X 链接 `libcflogger.a`(不传播),但 X 的 `.a` 里会包含对 cflogger 符号的引用 | -| `target_link_libraries(X PUBLIC cflogger_headers)` | X 及其消费者只获得 `cflog.h` 头文件路径,不链接任何 `.a` | - -`INTERFACE` 库本身没有编译产物(没有 `.a` / `.o`),它只是一个**编译期元数据载体**,传递 include 目录和编译定义。 - ---- - -## 3. 最终链接拓扑 - -### 3.1 完整依赖图 - -```text -cflogger (STATIC) ──────────────────────────────┐ - ├─ cflog.cpp, cflog_impl.cpp │ - ├─ console_sink.cpp, file_sink.cpp │ - ├─ console_formatter.cpp, file_formatter.cpp │ - └─ async_queue.cpp │ - │ --whole-archive -cflogger_headers (INTERFACE) ├────────────────→ CFDesktop_shared (DLL) - ├─ include dirs only │ - └─ links: cfbase │ - ↑ │ - │ use (headers only) │ - ├─ CFDesktopLog │ - ├─ CFDesktopEarlySession │ - ├─ CFDesktopPathSettings │ - ├─ cf_desktop_ui_platform │ - └─ cf_desktop_ui_widget_init_session │ - │ -CFDesktopMain (STATIC) ──PRIVATE──→ cflogger ───┘ - └─ 直接使用了 FileSink, ConsoleSink 等内部类 - 这些类没有 CFLOG_API 标记,不能通过 DLL 导入 -```bash - -### 3.2 各模块的角色 - -| 模块 | 如何使用 cflogger | 原因 | -|------|------------------|------| -| `CFDesktop_shared` | `--whole-archive` 链接 `cflogger` | 唯一的实际链接点,保证 DLL 内单实例 | -| `CFDesktopMain` | PRIVATE 链接 `cflogger` | `logger_stage.cpp` 直接实例化 `FileSink`/`ConsoleSink`,这些内部类没有 `CFLOG_API`,必须从静态库解析 | -| `CFDesktopLog` | PUBLIC 链接 `cflogger_headers` | 只调用 `CFLOG_INFO()` 等公开 API | -| `CFDesktopEarlySession` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `CFDesktopPathSettings` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `cf_desktop_ui_platform` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| `cf_desktop_ui_widget_init_session` | PUBLIC 链接 `cflogger_headers` | 只调用公开 API | -| Test / Example | `CFLOG_STATIC_BUILD` + 链接 `CFDesktop::logger` | 独立编译,不走 DLL 路径 | - ---- - -## 4. DLL 导出/导入机制 - -### 4.1 `CFLOG_API` 宏定义 - -定义在 `desktop/base/logger/include/cflog/cflog_export.h`: - -```cpp -#if defined(_WIN32) || defined(_MSC_VER) - #ifdef CFLOG_STATIC_BUILD - #define CFLOG_API // 测试/示例:无修饰 - #elif defined(CFLOG_BUILDING) - #define CFLOG_API __declspec(dllexport) // 构建 cflogger 时:导出 - #else - #define CFLOG_API __declspec(dllimport) // 消费者:导入 - #endif -#else - #define CFLOG_API __attribute__((visibility("default"))) // Linux:可见 -#endif -```bash - -### 4.2 标记规则 - -| 标记了 `CFLOG_API` 的 | 用途 | -|----------------------|------| -| `Logger` 类 (`cflog.hpp`) | 核心单例,EXE 通过 DLL 导入使用 | -| `trace()`, `debug()`, `info()`, `warning()`, `error()` 全局函数 | 公开日志 API | -| `set_level()`, `flush()` | 运行时配置 API | - -| 没有标记 `CFLOG_API` 的 | 原因 | -|------------------------|------| -| `ConsoleSink`, `FileSink` | 内部实现类,仅供 `CFDesktopMain` 的 `logger_stage.cpp` 使用 | -| `ConsoleFormatter`, `FileFormatter`, `AsciiColorFormatter` | 内部实现类 | -| `AsyncQueue` | 内部实现类 | - -### 4.3 为什么 `CFDesktopMain` 不能用 `cflogger_headers` - -如果 `CFDesktopMain` 改用 `cflogger_headers`,链接 `CFDesktop.exe` 时会报 `undefined symbol`: - -```text -ld.lld: error: undefined symbol: cf::log::FileSink::FileSink(...) -ld.lld: error: undefined symbol: vtable for cf::log::ConsoleSink -```yaml - -因为 `FileSink`、`ConsoleSink` 没有被 `CFLOG_API` 标记导出,EXE 无法通过 DLL 导入表找到它们。`CFDesktopMain` 必须 PRIVATE 链接 `cflogger` 静态库,让链接器直接从 `.a` 中解析这些符号。 - ---- - -## 5. LNK4217 警告修复:`CFLOG_STATIC_BUILD` 与 DLL 内部解析 - -### 5.1 问题现象 - -编译 `CFDesktop_shared.dll` 时,lld 链接器产生大量 LNK4217 警告: - -```text -ld.lld: warning: libCFDesktopMain.a(init_chain.cpp.obj): - locally defined symbol imported: - cf::log::Logger::instance() - (defined in libcflogger.a(cflog.cpp.obj)) [LNK4217] -```text - -涉及 `.a` 文件:`libCFDesktopMain.a`、`libCFDesktopUi.a`、`libcf_desktop_ui_platform.a`。 - -### 5.2 根因分析 +CFDesktop 采用"多静态库合并为单一共享库"架构:所有静态库通过 `--whole-archive` 打包进 `CFDesktop_shared.dll`,EXE 只链接这个 DLL。`cflogger` 使用单例模式,如果同一份 `.cpp` 被编译进多个模块,会产生多个独立实例,导致日志输出丢失和全局状态不一致。 -LLD 警告的含义是:**某 `.obj` 文件通过 `__declspec(dllimport)` 引用了一个符号,但该符号在同一 DLL 内有本地定义。** +核心问题在于:DLL 内部的模块需要调用日志 API(如 `CFLOG_INFO()`),但不应各自链接 `cflogger` 静态库实体。解决方案是引入 `cflogger_headers` 这个 CMake `INTERFACE` 库——它没有编译产物,仅传递 include 目录和编译定义。DLL 内部模块链接 `cflogger_headers` 只拿头文件,唯一实际链接 `cflogger` 静态库的地方是 `CFDesktop_shared`(通过 `--whole-archive`)。这保证了 `Logger` 实现只编译一次,单例在整个进程中唯一。 -具体链路: +`--whole-archive` 在此架构中不可省略:它强制链接器保留 `cflogger` 的所有符号(包括未被直接引用的全局对象构造函数),否则链接器会丢弃"未使用"的 Logger 实现。`CFLOG_STATIC_BUILD` 宏作为补充手段,消除 DLL 内部 `.obj` 文件通过 `__declspec(dllimport)` 引用本地符号时产生的 LNK4217 警告。 -```text -cflog_export.h 中的宏展开逻辑: +## 关键决策 - CFLOG_BUILDING → __declspec(dllexport) ← 仅 cflogger 自身编译时 - CFLOG_STATIC_BUILD → (空) ← 测试/示例用 - 默认 → __declspec(dllimport) ← 其他所有消费者 -```text +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| `cflogger_headers` INTERFACE 库隔离头文件与链接实体 | `.cpp` 只编译一次,`.h` 可以到处传,零运行时开销 | OBJECT library:需要所有使用方从同一 target 获取 `.o`,管理复杂度高 | +| `--whole-archive` 打包所有静态库进单一 DLL | 保证单例唯一且所有符号可被 EXE 通过 DLL 导入表访问 | 去掉 DLL 全静态链接:改动量大,不符合项目共享库架构 | +| `CFLOG_STATIC_BUILD` PRIVATE 编译定义 | DLL 内模块用本地引用,EXE 用 `dllimport`,各取所需 | PUBLIC 编译定义:会传播给 EXE 导致链接失败 | +| `CFLOG_API` 标记公开类,内部类不标记 | `Logger`/全局日志函数通过 DLL 导出,`FileSink`/`ConsoleSink` 等内部类仅供 `CFDesktopMain` 直接从静态库解析 | 所有类都标记导出:暴露实现细节,增加 ABI 维护负担 | +| `CFDesktopMain` PRIVATE 链接 `cflogger`(非 headers) | 直接使用 `FileSink`/`ConsoleSink` 等未导出的内部类,必须从静态库解析符号 | 改用 headers:链接时 undefined symbol | -DLL 内的静态库(如 `CFDesktopMain`)编译时: -- 没有定义 `CFLOG_BUILDING`(那是 cflogger 自己的) -- 没有定义 `CFLOG_STATIC_BUILD`(之前没加) -- 所以 `CFLOG_API` = `__declspec(dllimport)` - -但链接时,这些 `.obj` 文件和 `libcflogger.a` 被 `--whole-archive` 打进**同一个 DLL**。链接器发现:这些 `dllimport` 引用的符号其实就在本地。LLD 对此发出警告。 - -> **这不是多实例问题**——链接器确实使用了本地定义,运行时行为正确。 -> 但警告噪音大,且暗示链接意图不清晰。 - -### 5.3 修复方案 - -给所有会打入 `CFDesktop_shared` 的静态库目标添加 `CFLOG_STATIC_BUILD` **PRIVATE** 编译定义: - -```cmake -target_compile_definitions(CFDesktopMain PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(CFDesktopUi PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(cf_desktop_ui_platform PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(CFDesktopLog PRIVATE CFLOG_STATIC_BUILD) -target_compile_definitions(cf_desktop_ui_widget_init_session PRIVATE CFLOG_STATIC_BUILD) -```bash - -关键:**必须是 PRIVATE**,不能是 PUBLIC 或 INTERFACE。 - -- `PRIVATE` → 只作用于该目标的 `.cpp` 文件,不传播给消费者 -- 如果用 PUBLIC → EXE 也会得到 `CFLOG_STATIC_BUILD`,导致 `CFLOG_API` = 空,EXE 不再 `dllimport`,链接失败 - -### 5.4 修复后的宏展开 - -| 编译目标 | 宏定义 | `CFLOG_API` 展开 | 效果 | -|---------|--------|-----------------|------| -| `cflogger`(静态库) | `CFLOG_BUILDING` | `__declspec(dllexport)` | 符号从 DLL 导出 | -| `CFDesktopMain`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `CFDesktopUi`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `cf_desktop_ui_platform`(静态库) | `CFLOG_STATIC_BUILD` | *(空)* | 本地引用,无 dllimport | -| `CFDesktop.exe` | *(无)* | `__declspec(dllimport)` | 从 DLL 导入,正确 | - -### 5.5 判断哪些目标需要添加 - -原则:**所有 `.obj` 文件会通过 `--whole-archive` 进入 DLL、且 `#include` 了 `cflog.h` 的静态库目标。** - -可通过编译输出中的 LNK4217 警告定位(警告里提到的 `.a` 文件对应的目标)。 - -不链接 cflogger 的目标(如 `cf_desktop_render`、`cfbase`)无需添加。 - ---- - -## 6. 单实例保证原理 - -### 6.1 在 DLL 架构下 - -```text -cflogger.cpp 编译一次 → libcflogger.a (静态库) - ↓ - --whole-archive 合并 - ↓ - CFDesktop_shared.dll (唯一包含 Logger 实现的模块) - -CFDesktop.exe 通过 dllimport 访问 Logger::instance() -CFDesktopMain.a 通过 PRIVATE 链接直接解析内部类符号 -其他 .a 通过 cflogger_headers 只拿到头文件,不链接 .a -```text - -`--whole-archive` 确保即使没有直接引用的符号也被保留在 DLL 中,避免链接器丢弃"未使用"的 Logger 实现。 - -### 6.2 验证方法 - -在代码中打印 Logger 实例地址,确认所有模块使用同一个: - -```cpp -printf("Logger: %p\n", &Logger::instance()); -```yaml - -如果在 DLL 中调用和 EXE 中调用得到的地址相同,说明单实例保证生效。 - ---- - -## 7. CMake 关键决策记录 - -### 7.1 为什么不用 OBJECT library - -```cmake -# 可选方案(未采用) -add_library(cflog_obj OBJECT cflog.cpp ...) -target_sources(CFDesktopMain PRIVATE $) -```text - -OBJECT library 要求所有使用 cflogger 的模块都从同一个 OBJECT target 获取 `.o` 文件,这在当前多模块架构中引入更多管理复杂度。当前的 `STATIC + --whole-archive` 方案已经足够保证单实例。 - -### 7.2 为什么不用 header-only 单例 - -```cpp -// 危险!不要这样做 -inline Logger& instance() { - static Logger x; // 每个 TU 可能独立实例化 - return x; -} -```bash - -`inline` 函数中的 `static` 局部变量在 C++17 前的行为是 UB(多个 TU 可能各自实例化),即使在 C++17 后也有跨动态库的复杂性。始终让 `.cpp` 文件只编译一次是最安全的做法。 - -### 7.3 未来如果去掉 DLL - -如果将来改为全静态单进程架构(无 DLL),修改量极小: - -1. `CFDesktop_shared` 从 `SHARED` 改为 `STATIC` -2. `CFDesktop.exe` 直接链接所有静态库(不需要 `--whole-archive`) -3. 移除 `CFLOG_API` 宏(或全部定义为空) -4. `cflogger_headers` 方案完全不需要改 - ---- - -## 8. 修改的文件清单 - -| 文件 | 改动内容 | -|------|---------| -| `desktop/base/logger/CMakeLists.txt` | 新增 `cflogger_headers` INTERFACE target | -| `desktop/main/log/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/main/early_session/CMakeLists.txt` | `cflogger` → `cflogger_headers` | -| `desktop/main/path/CMakeLists.txt` | `cflogger` → `cflogger_headers` | -| `desktop/main/CMakeLists.txt` | 添加 `CFLOG_STATIC_BUILD` PRIVATE;保持 `cflogger` PRIVATE | -| `desktop/ui/CMakeLists.txt` | 添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/ui/platform/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | -| `desktop/ui/widget/init_session/CMakeLists.txt` | `cflogger` → `cflogger_headers`;添加 `CFLOG_STATIC_BUILD` PRIVATE | - ---- - -## 9. 扩展建议 - -### 9.1 未来新模块使用 cflogger 的规则 - -- 如果只调用 `CFLOG_INFO()` 等公开 API → 链接 `cflogger_headers` -- 如果直接实例化 `FileSink`/`ConsoleSink` 等内部类 → PRIVATE 链接 `cflogger`(仅限 DLL 内部模块) -- 测试/示例 → `CFLOG_STATIC_BUILD` + `CFDesktop::logger` -- **新模块如果会进入 `CFDesktop_shared` DLL** → 必须添加 `CFLOG_STATIC_BUILD` PRIVATE(参见第 5 节) - -### 9.2 适用于其他核心系统 - -同样的模式可以用于事件系统、配置系统等需要单实例保证的核心模块: - -```cmake -# 事件系统示例 -add_library(cfevent STATIC ...) -add_library(cfevent_headers INTERFACE ...) -target_include_directories(cfevent_headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) -```yaml - -**原则:`.cpp` 只编译一次,`.h` 可以到处传。** - ---- +## 当前状态 -*本文档编写于 2026-03-30,基于 CFDesktop 项目 feat/windows_backend 分支的实际架构改动。* +已实现并验证。`cflogger_headers` 定义于 `desktop/base/logger/CMakeLists.txt`,所有 DLL 内部模块已迁移至链接 headers。该模式可作为事件系统、配置系统等其他核心单例模块的参考模板。 diff --git a/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md index 8c132fabf..36b7de792 100644 --- a/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md +++ b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md @@ -1,157 +1,23 @@ -# PolicyChain Clang 18 Miscompilation — Debug Progress +--- +title: PolicyChain Clang 18 Miscompilation +description: Clang 18 -O3 对 std::optional 返回值的寄存器打包缺陷导致 policy_chain 行为异常的根因与修复 +--- -## 问题描述 +# PolicyChain Clang 18 Miscompilation -- 设计意图 -CI 中 Clang 18 (Ubuntu 24.04 Docker, Release/-O3) 有 4 个 `policy_chain_test` 测试失败, -GCC 和 Clang 22 均通过。 +## 为什么需要 workaround -**失败的测试:** -- `PolicyChainTest.Fallback_SecondPolicy` — `execute(-3)` 返回 `-6`(期望 `3`) -- `PolicyChainTest.Fallback_MultiplePolicies` — `execute(-5)` 返回 `-10`(期望 `0`) -- `PolicyChainTest.MakePolicyChain_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) -- `PolicyChainTest.Builder_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) +Clang 18 在 `-O3` 下将 `std::optional` 的小对象返回值打包进寄存器时存在缺陷:当 lambda 返回 `std::nullopt` 时,`x * 2` 的 64-bit 计算结果通过 `lea` 指令产生进位,污染了 packed register 中的 engaged byte(bit 32)。调用方通过 `has_value()` 检测时看到 `has_value == true`,实际值却是 `-6` 而非预期的 fallback 到下一条 policy。GCC 和 Clang 19+ 不受影响。 -**共同模式:** 第一个 policy 的 `if (x > 0)` 条件被 Clang 18 优化掉, -`x * 2` 被无条件执行,`std::nullopt` 返回路径被消除。 +修复在 `PolicyModel::invoke()` 中:接收 `std::invoke()` 结果后,对 Clang < 19 显式重建 nullopt 路径(`if (!result.has_value()) return std::nullopt;`),强制编译器保留 disengaged 分支。 -## 复现命令(Docker) +## 关键决策 -```bash -# 构建 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc 'ccache -C; bash scripts/build_helpers/linux_fast_develop_build.sh ci -c build_ci_clang_config.ini' +| 决策 | 理由 | 被否决的替代方案 | +|------|------|------------------| +| 在 `invoke()` 内显式重建 nullopt 路径 | 最小侵入,只影响 Clang 18 的代码生成,不影响其他编译器 | `noinline/optnone` 屏障:无法深入保护 lambda 内部的返回值打包 | +| `#if` 条件编译限制为 Clang < 19 | 已确认 Clang 19+ 修复了此寄存器打包问题 | 全平台加屏障:不必要的性能退化 | -# 跑单个失败测试 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc './out/build_ci_clang/test/bin/policy_chain_test --gtest_filter="PolicyChainTest.Fallback_SecondPolicy"' +## 当前状态 -# 跑全部 policy_chain 测试 -docker run --rm -v "$PWD":/project -w /project \ - ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ - bash -lc 'ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure' -``` - -## 关键文件 - -- **实现:** `base/include/base/policy_chain/policy_chain.hpp` -- **测试:** `test/base/policy_chain/policy_chain_test.cpp` - -## 已有的 Workaround(不够) - -文件中已有 `invoke_policy` barrier(第 30-41 行): - -```cpp -#if defined(__clang__) && (__clang_major__ < 19) -# define CF_POLICY_CHAIN_INVOKE_BARRIER __attribute__((noinline, optnone)) -#else -# define CF_POLICY_CHAIN_INVOKE_BARRIER -#endif - -template -[[nodiscard]] CF_POLICY_CHAIN_INVOKE_BARRIER auto -invoke_policy(Policy const& policy, CallArgs&&... args) - -> decltype(policy(std::forward(args)...)) { - return policy(std::forward(args)...); -} -``` - -这个 barrier 无效——问题不在 `execute()` 的 `has_value()` 检查,而在更深层。 - -## 调试发现(通过 fprintf 日志定位) - -### 实验 1:在 `execute()` 和 `PolicyModel::invoke()` 都加 fprintf → PASS -### 实验 2:只在 `execute()` 加 fprintf,`PolicyModel::invoke()` 不加 → FAIL - -**关键日志(失败时):** -``` -execute(-3): - [DEBUG] invoke_policy returned, has_value=1 ← 只调了一次! - [DEBUG] returning value: -6 -``` - -**正确行为(加 fprintf 后):** -``` -execute(-3): - [DEBUG] PolicyModel::invoke(), has_value=0, val=0 ← 第一个 policy 返回 nullopt - [DEBUG] PolicyModel::invoke(), has_value=1, val=3 ← 第二个 policy 返回 3 - [DEBUG] returning value: 3 -``` - -**结论:** 第一个 policy 的 lambda 被错误编译——`if (x > 0)` 条件被消除。 -fprintf 在 `PolicyModel::invoke()` 中作为副作用阻止了该错误优化。 - -## 已尝试但无效的修复 - -1. `__attribute__((noinline, optnone))` on `PolicyModel::invoke()` — 编译错误(与 `[[nodiscard]]` 冲突) -2. `[[clang::noinline]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 -3. `[[clang::optnone]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 -4. `[[gnu::noinline, gnu::optnone]]` — Clang 不认识 `[[gnu::optnone]]`,被忽略 - -## 调用链分析 - -``` -PolicyChain::execute(args) - → policy_chain_detail::invoke_policy(policy, args) [noinline, optnone] ← 无效 - → PolicyEntry::operator()(args) - → PolicyConcept::invoke(args) [virtual dispatch] - → PolicyModel::invoke(args) ← lambda 在这里被调用 - → std::invoke(policy_, args...) ← lambda body 被错误优化 -``` - -**根因:** Clang 18 `-O3` 在编译 `PolicyModel::invoke()` 时, -将 lambda body 内联后错误优化掉了条件分支。`invoke_policy` 的 barrier 在调用链外层, -无法保护 `invoke()` 内部的 lambda 编译。 - -## 根因分析(2026-05-23 反汇编确认) - -反汇编显示,lambda 的条件分支并不是简单被删除,而是 Clang 18 在 `-O3` -下把 `std::optional` 的小对象返回值打包进寄存器时**污染了 engaged byte**。 - -失败用例的第一条 policy 生成了类似逻辑: - -```asm -xor %eax,%eax -test %edi,%edi -setg %al -shl $0x20,%rax ; has_value 放到 bit 32 -mov %edi,%ecx -lea (%rax,%rcx,2),%rax -``` - -当 `x == -3` 时,`x * 2` 的 64-bit `lea` 结果为 `0x00000001fffffffa`, -bit 32 被算术进位置 1,导致 disengaged optional 被调用方看成 -`has_value == true`,值为 `-6`。 - -### 试过但无效 - -- 把 lambda 调用包进新的 `noinline, optnone invoke_stored_policy()`:无效,因为 - lambda `operator()` 仍会作为单独 O3 函数生成坏的返回打包。 -- `std::function(int)>` 最小复现:同样失败。 -- `asm volatile("" : "+m"(result) : : "memory")`:无效,只是把已经污染的 - packed register 存回内存。 - -## 有效修复 - -在 `PolicyModel::invoke()` 中先接住 `std::invoke()` 的结果,再针对 Clang < 19 -显式重建 nullopt 路径: - -```cpp -auto result = std::invoke(policy_, args...); -#if defined(__clang__) && (__clang_major__ < 19) -if (!result.has_value()) { - return std::nullopt; -} -#endif -return result; -``` - -这会让 Clang 18 保留 disengaged 分支,避免直接复用被污染的 packed register。 - -## 验证 - -- Docker/CI Clang 18:`ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure` - 全部通过,1/1 test passed。 -- 本地 develop 构建:`./out/build_develop/test/bin/policy_chain_test` - 全部通过,29/29 tests passed。 +已修复并验证。CI Docker Clang 18 和本地 develop 构建均通过。相关代码位于 `base/include/base/policy_chain/policy_chain.hpp`。 diff --git a/document/notes/README.md b/document/notes/README.md deleted file mode 100644 index 0f1ba2f75..000000000 --- a/document/notes/README.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: CFDesktop 桌面行为系统设计文档 -description: 一套完整的跨平台桌面应用窗口行为管理架构设计方案 ---- - -# CFDesktop 桌面行为系统设计文档 - -> 一套完整的跨平台桌面应用窗口行为管理架构设计方案 - -## 概述 - -本文档系列详细阐述了 CFDesktop 项目中窗口行为建模、Qt 集成、策略模式应用和系统架构设计的完整方案。该架构通过分层设计和策略模式,实现了可扩展、跨平台、插件化的窗口行为管理系统。 - -## 核心特性 - -- **类型安全**:基于 Qt `QFlags` 的类型安全行为建模 -- **跨平台**:统一的抽象层,支持 Windows、macOS、Linux (X11/Wayland)、Embedded -- **可扩展**:插件化架构,动态加载行为策略 -- **可测试**:依赖抽象接口,便于编写单元测试 -- **冲突可控**:完善的冲突检测与解决机制 - -## 文档导航 - -| 文档 | 描述 | 链接 | -|------|------|------| -| **01** | 桌面行为建模:从 bool 到 QFlags | [README](01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md) | -| **02** | Qt 窗口行为解析:QWidget → DesktopBehaviors | [README](02-Qt-Window-Behavior-Analysis.md) | -| **03** | 桌面策略系统设计:Strategy Pattern 实战 | [README](03-Desktop-Strategy-Pattern-Design.md) | -| **04** | 桌面行为系统设计:从策略到 Window Manager 抽象 | [README](04-Desktop-Behavior-System-Architecture.md) | - -## 快速开始 - -### 安装 - -将文档克隆到本地或直接在项目中查看: - -```bash -cd /home/charliechen/CFDesktop/document/notes -```text - -### 阅读顺序 - -```text -新手开发者: - 01 → 02 → 03 → 04 - -有经验开发者: - 直接阅读 04,需要时查阅其他文档 -```text - -### 代码示例 - -```cpp -// 定义行为标志 -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - // ... -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) - -// 使用行为组合 -DesktopBehaviors behaviors = DesktopBehaviorFlag::Fullscreen - | DesktopBehaviorFlag::Frameless; - -// 测试行为 -if (behaviors.testFlag(DesktopBehaviorFlag::Fullscreen)) { - // 处理全屏逻辑 -} -```text - -## 架构概览 - -```text -┌─────────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ (用户代码 / 业务逻辑) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Behavior Abstraction Layer │ -│ (DesktopBehaviors - QFlags) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Strategy Layer │ -│ (IDesktopBehaviorStrategy - 插件化) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Qt Integration Layer │ -│ (Qt WindowFlags / QWidget) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Platform Abstraction Layer │ -│ (Windows / macOS / X11 / Wayland / Embedded) │ -└─────────────────────────────────────────────────────────────┘ -```bash - -## 核心概念 - -### DesktopBehaviors - -使用 `QFlags` 定义的行为标志集合,用于描述窗口的各种行为特性。 - -### Strategy Pattern - -将每种行为封装为独立的策略类,实现可插拔的行为管理。 - -### Conflict Resolution - -自动检测和解决行为冲突,确保系统状态一致性。 - -### Plugin System - -支持动态加载行为策略插件,扩展系统能力。 - -## 技术栈 - -| 技术 | 版本 | 用途 | -|------|------|------| -| Qt | 6.x | GUI 框架 | -| C++ | 17+ | 编程语言 | -| QFlags | - | 类型安全标志 | -| QPluginLoader | - | 插件加载 | - -## 平台支持 - -| 平台 | 状态 | 备注 | -|------|------|------| -| Windows | ✅ 完全支持 | 所有特性可用 | -| macOS | ✅ 完全支持 | 所有特性可用 | -| Linux (X11) | ✅ 完全支持 | 所有特性可用 | -| Linux (Wayland) | ⚠️ 部分支持 | 部分特性受限 | -| Embedded | ⚠️ 部分支持 | 取决于具体平台 | - -## 项目结构 - -```text -desktop/ -├── ui/ -│ ├── CFDesktop.cpp # 主窗口实现 -│ ├── CFDesktop.h -│ └── platform/ # 平台特定代码 -│ ├── linux_wsl/ # WSL/Linux 策略 -│ ├── windows/ # Windows 策略 -│ └── ... -├── main/ -│ └── init/ # 初始化代码 -└── ... - -document/notes/ # 本文档目录 -├── README.md -├── index.md -├── 01-Desktop-Behavior-Modeling-From-Bool-To-QFlags.md -├── 02-Qt-Window-Behavior-Analysis.md -├── 03-Desktop-Strategy-Pattern-Design.md -└── 04-Desktop-Behavior-System-Architecture.md -```yaml - -## 贡献指南 - -本文档是 CFDesktop 项目的技术文档,欢迎团队成员: - -1. 报告文档错误或不清晰的地方 -2. 提出新的文档需求 -3. 补充代码示例 -4. 分享使用经验 - -## 许可证 - -本文档为 CFDesktop 项目的内部技术文档。 - -## 参考资源 - -- [Qt 官方文档](https://doc.qt.io/qt-6/) -- [QFlags Class Reference](https://doc.qt.io/qt-6/qflags.html) -- [Qt Platform Abstraction](https://doc.qt.io/qt-6/qpa.html) -- [Strategy Pattern - Refactoring.Guru](https://refactoring.guru/design-patterns/strategy) - ---- - -**更新时间**:2025-03-27 - -**维护者**:CFDesktop Team diff --git a/document/notes/index.md b/document/notes/index.md index 0254c0f99..4a1e84f4f 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -89,42 +89,44 @@ description: 本文档系列详细介绍了桌面应用程序中窗口行为建 --- -## 核心概念 +## 架构概览 -### DesktopBehaviors - -使用 `QFlags` 定义的行为标志集合: - -```cpp -enum class DesktopBehaviorFlag { - None = 0, - Fullscreen = 1 << 0, - Frameless = 1 << 1, - StayOnTop = 1 << 2, - StayOnBottom = 1 << 3, - AllowResize = 1 << 4, - AvoidSystemUI = 1 << 5, - // ... -}; - -Q_DECLARE_FLAGS(DesktopBehaviors, DesktopBehaviorFlag) ```text - -### 架构分层 +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (用户代码 / 业务逻辑) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Behavior Abstraction Layer │ +│ (DesktopBehaviors - QFlags) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Strategy Layer │ +│ (IDesktopBehaviorStrategy - 插件化) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Qt Integration Layer │ +│ (Qt WindowFlags / QWidget) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Platform Abstraction Layer │ +│ (Windows / macOS / X11 / Wayland / Embedded) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 阅读顺序 ```text -Application Layer (用户代码) - ↓ -Behavior Abstraction (DesktopBehaviors) - ↓ -Strategy Layer (IDesktopBehaviorStrategy) - ↓ -Qt Integration (WindowFlags) - ↓ -Platform Abstraction (Windows/X11/Wayland) -```bash +新手开发者: + 01 → 02 → 03 → 04 ---- +有经验开发者: + 直接阅读 04,需要时查阅其他文档 +``` ## 参考资源 @@ -148,39 +150,3 @@ Platform Abstraction (Windows/X11/Wayland) - [Layered Architecture - Medium](https://medium.com/@patrykrogedu/layered-architecture-and-abstraction-layers-167438dd1a8b) - [Software Architecture Patterns - Red Hat](https://www.redhat.com/en/blog/14-software-architecture-patterns) - ---- - -## 快速开始 - -1. **阅读顺序建议**: - - 初学者:01 → 02 → 03 → 04 - - 有经验开发者:可直接阅读 04,需要时再查阅其他文档 - -2. **实践建议**: - - 在阅读文档时,参考您的项目代码:`desktop/ui/platform/` - - 尝试将现有代码重构为文档中描述的架构 - - 使用 Mock 对象进行单元测试 - -3. **扩展建议**: - - 根据项目需求定义额外的 `DesktopBehaviorFlag` - - 实现平台特定的 Strategy - - 开发自定义插件 - ---- - -## 版本历史 - -| 版本 | 日期 | 说明 | -|------|------|------| -| 1.0.0 | 2025-03-27 | 初始版本,包含四篇核心文档 | - ---- - -## 许可 - -本文档系列为 CFDesktop 项目的内部技术文档,仅供项目开发和维护使用。 - ---- - -*本文档由 Claude (Anthropic) 协助编写,基于项目实际代码和 Qt 官方文档整理。* diff --git a/document/scripts/README.md b/document/scripts/README.md deleted file mode 100644 index 14e5e25ea..000000000 --- a/document/scripts/README.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Scripts文档 -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目所有脚本的完整文档。" ---- - -# Scripts文档 - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop项目所有脚本的完整文档。 - -## 目录结构 - -```text -scripts/ -├── build_helpers/ # 构建辅助脚本 (Linux/Windows) -├── dependency/ # 依赖安装 -├── develop/ # 开发工具 (代码格式化、清理) -├── docker/ # Docker配置 -├── doxygen/ # Doxygen工具 -├── lib/ # 库文件 -│ ├── bash/ # Bash库 -│ └── powershell/ # PowerShell库 -├── release/ # 发布相关 -│ └── hooks/ # Git钩子 -└── run_helpers/ # 运行辅助 -```bash - -## 快速导航 - -### 构建相关 -- [构建辅助脚本](build_helpers/) - Linux/Windows构建脚本 - -### 开发工具 -- [依赖安装](dependency/install_build_dependencies.sh.md) - 环境配置 -- [代码格式化](develop/format_cpp.sh.md) - C++代码格式化 -- [空格清理](develop/remove_trailing_space.sh.md) - 删除行尾空格 - -### 容器化 -- [Docker配置](docker/Dockerfile.build.md) - 构建环境镜像 - -### 库文件 -- [Bash库](lib/bash/) - Bash函数库 -- [PowerShell库](lib/powershell/) - PowerShell模块 - -### 版本控制 -- [Git钩子](release/hooks/) - Git hooks配置 - -## 文档规范 - -每个脚本文档包含: -- **使用办法**: 基本语法、参数、示例 -- **Scripts详解**: 用途、依赖、核心功能 - -## 脚本语言 - -| 平台 | 脚本类型 | -|------|----------| -| Linux/macOS | Bash (.sh) | -| Windows | PowerShell (.ps1) | - -## 相关文档 - -- [开发指南](../../development/) - 项目开发文档 -- [CI/CD文档](../../ci/) - 持续集成文档 diff --git a/document/scripts/build_helpers/README.md b/document/scripts/build_helpers/README.md deleted file mode 100644 index f9c24a925..000000000 --- a/document/scripts/build_helpers/README.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: 构建辅助脚本 (Build Helpers) -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop项目的构建辅助脚本。" ---- - -# 构建辅助脚本 (Build Helpers) - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop项目的构建辅助脚本。 - -## Linux脚本 - -| 脚本 | 说明 | -|------|------| -| ci_build_entry.sh | CI构建入口 | -| linux_configure.sh | CMake配置 | -| linux_develop_build.sh | 完整开发构建 | -| linux_fast_develop_build.sh | 快速开发构建 | -| linux_deploy_build.sh | 完整部署构建 | -| linux_fast_deploy_build.sh | 快速部署构建 | -| linux_run_tests.sh | 运行测试 | -| docker_start.sh | Docker启动脚本 | - -## 构建类型 - -| 类型 | 说明 | 配置文件 | -|------|------|----------| -| develop | 开发构建,包含调试符号 | build_develop_config.ini | -| deploy | 部署构建,优化体积 | build_deploy_config.ini | -| fast_develop | 快速开发构建,增量编译 | build_develop_config.ini | -| fast_deploy | 快速部署构建,增量编译 | build_deploy_config.ini | - -## 配置文件 - -| 文件 | 说明 | -|------|------| -| build_develop_config.ini | 开发构建配置 | -| build_deploy_config.ini | 部署构建配置 | -| build_ci_config.ini | CI构建配置 | -| build_ci_aarch64_config.ini | ARM64 CI配置 | -| build_ci_armhf_config.ini | ARM HF CI配置 | - -## 快速开始 - -### 开发构建 - -```bash -# 完整开发构建(清理后构建) -./scripts/build_helpers/linux_develop_build.sh - -# 快速开发构建(增量编译) -./scripts/build_helpers/linux_fast_develop_build.sh -```text - -### 部署构建 - -```bash -# 完整部署构建(清理后构建) -./scripts/build_helpers/linux_deploy_build.sh - -# 快速部署构建(增量编译) -./scripts/build_helpers/linux_fast_deploy_build.sh -```text - -### 仅配置 - -```bash -./scripts/build_helpers/linux_configure.sh [develop|deploy|ci] -```text - -### 运行测试 - -```bash -./scripts/build_helpers/linux_run_tests.sh [develop|deploy|ci] -```text - -### Docker构建 - -```bash -# 交互式shell -./scripts/build_helpers/docker_start.sh - -# CI构建验证 -./scripts/build_helpers/docker_start.sh --verify - -# 构建项目(完整清理) -./scripts/build_helpers/docker_start.sh --build-project - -# 构建项目(快速) -./scripts/build_helpers/docker_start.sh --build-project-fast - -# 运行测试 -./scripts/build_helpers/docker_start.sh --run-project-test -```bash - -## 架构支持 - -脚本支持多架构构建: - -| 架构 | 平台 | 说明 | -|------|------|------| -| x86_64 / amd64 | linux/amd64 | 标准PC架构 | -| aarch64 | linux/arm64 | ARM64架构 | -| armv7l / armhf | linux/armhf | ARM32架构 (IMX6ULL) | - -CI构建脚本会自动检测容器架构并选择相应的配置文件。 - -## 依赖库 - -所有脚本依赖以下公共库(位于 `scripts/lib/bash/`): - -- `lib_common.sh` - 通用日志和工具函数 -- `lib_config.sh` - 配置文件处理 -- `lib_paths.sh` - 路径解析 -- `lib_build.sh` - 构建相关函数 -- `lib_args.sh` - 参数解析 - -## 退出码 - -- `0` - 成功 -- `非0` - 失败(具体错误码取决于失败阶段) diff --git a/document/scripts/build_helpers/index.md b/document/scripts/build_helpers/index.md index 8195b0368..7e2136bb9 100644 --- a/document/scripts/build_helpers/index.md +++ b/document/scripts/build_helpers/index.md @@ -1,12 +1,105 @@ --- title: 构建辅助脚本 -description: 本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShel +description: 本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 --- # 构建辅助脚本 -本目录包含平台相关的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 +本目录包含 CFDesktop 项目的构建辅助脚本,提供 Linux(Bash)和 Windows(PowerShell)双平台的 CMake 配置、快速开发构建、完整构建以及测试运行等标准化构建流程。同时提供 Docker 环境下的构建脚本以确保跨平台一致性。 ---- +## Linux脚本 + +| 脚本 | 说明 | +|------|------| +| ci_build_entry.sh | CI构建入口 | +| linux_configure.sh | CMake配置 | +| linux_develop_build.sh | 完整开发构建 | +| linux_fast_develop_build.sh | 快速开发构建 | +| linux_deploy_build.sh | 完整部署构建 | +| linux_fast_deploy_build.sh | 快速部署构建 | +| linux_run_tests.sh | 运行测试 | +| docker_start.sh | Docker启动脚本 | + +## 构建类型 + +| 类型 | 说明 | 配置文件 | +|------|------|----------| +| develop | 开发构建,包含调试符号 | build_develop_config.ini | +| deploy | 部署构建,优化体积 | build_deploy_config.ini | +| fast_develop | 快速开发构建,增量编译 | build_develop_config.ini | +| fast_deploy | 快速部署构建,增量编译 | build_deploy_config.ini | + +## 配置文件 + +| 文件 | 说明 | +|------|------| +| build_develop_config.ini | 开发构建配置 | +| build_deploy_config.ini | 部署构建配置 | +| build_ci_config.ini | CI构建配置 | +| build_ci_aarch64_config.ini | ARM64 CI配置 | +| build_ci_armhf_config.ini | ARM HF CI配置 | + +## 快速开始 + +### 开发构建 + +```bash +# 完整开发构建(清理后构建) +./scripts/build_helpers/linux_develop_build.sh + +# 快速开发构建(增量编译) +./scripts/build_helpers/linux_fast_develop_build.sh +``` + +### 部署构建 + +```bash +# 完整部署构建(清理后构建) +./scripts/build_helpers/linux_deploy_build.sh + +# 快速部署构建(增量编译) +./scripts/build_helpers/linux_fast_deploy_build.sh +``` + +### 仅配置 + +```bash +./scripts/build_helpers/linux_configure.sh [develop|deploy|ci] +``` + +### 运行测试 + +```bash +./scripts/build_helpers/linux_run_tests.sh [develop|deploy|ci] +``` + +### Docker构建 + +```bash +# 交互式shell +./scripts/build_helpers/docker_start.sh + +# CI构建验证 +./scripts/build_helpers/docker_start.sh --verify + +# 构建项目(完整清理) +./scripts/build_helpers/docker_start.sh --build-project + +# 构建项目(快速) +./scripts/build_helpers/docker_start.sh --build-project-fast + +# 运行测试 +./scripts/build_helpers/docker_start.sh --run-project-test +``` + +## 架构支持 + +脚本支持多架构构建: + +| 架构 | 平台 | 说明 | +|------|------|------| +| x86_64 / amd64 | linux/amd64 | 标准PC架构 | +| aarch64 | linux/arm64 | ARM64架构 | +| armv7l / armhf | linux/armhf | ARM32架构 (IMX6ULL) | -*Last updated: 2026-03-20* +CI构建脚本会自动检测容器架构并选择相应的配置文件。 diff --git a/document/scripts/index.md b/document/scripts/index.md index 68facdca5..566e2446b 100644 --- a/document/scripts/index.md +++ b/document/scripts/index.md @@ -1,12 +1,57 @@ --- title: 脚本工具 -description: 本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / +description: 本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 --- # 脚本工具 -本目录包含 CFDesktop 项目全部脚本工具的文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 +本目录包含 CFDesktop 项目所有脚本的完整文档,涵盖构建辅助脚本(Linux Bash / Windows PowerShell)、Docker 构建与部署脚本、第三方依赖管理脚本、开发工作流辅助工具、Doxygen 文档生成脚本以及正式发布流程脚本。 ---- +## 目录结构 + +```text +scripts/ +├── build_helpers/ # 构建辅助脚本 (Linux/Windows) +├── dependency/ # 依赖安装 +├── develop/ # 开发工具 (代码格式化、清理) +├── docker/ # Docker配置 +├── doxygen/ # Doxygen工具 +├── lib/ # 库文件 +│ ├── bash/ # Bash库 +│ └── powershell/ # PowerShell库 +├── release/ # 发布相关 +│ └── hooks/ # Git钩子 +└── run_helpers/ # 运行辅助 +``` + +## 快速导航 + +### 构建相关 +- [构建辅助脚本](build_helpers/) - Linux/Windows构建脚本 + +### 开发工具 +- [依赖安装](dependency/install_build_dependencies.sh.md) - 环境配置 +- [代码格式化](develop/format_cpp.sh.md) - C++代码格式化 +- [空格清理](develop/remove_trailing_space.sh.md) - 删除行尾空格 + +### 容器化 +- [Docker配置](docker/Dockerfile.build.md) - 构建环境镜像 + +### 库文件 +- [Bash库](lib/bash/) - Bash函数库 +- [PowerShell库](lib/powershell/) - PowerShell模块 + +### 版本控制 +- [Git钩子](release/hooks/) - Git hooks配置 + +## 脚本语言 + +| 平台 | 脚本类型 | +|------|----------| +| Linux/macOS | Bash (.sh) | +| Windows | PowerShell (.ps1) | + +## 相关文档 -*Last updated: 2026-03-20* +- [开发指南](../../development/) - 项目开发文档 +- [CI/CD文档](../../ci/) - 持续集成文档 diff --git a/document/scripts/lib/bash/README.md b/document/scripts/lib/bash/README.md deleted file mode 100644 index e7051884a..000000000 --- a/document/scripts/lib/bash/README.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Bash库文档 -description: "文档编写日期: 2026-03-20,本目录包含CFDesktop构建系统使用的Bash库文件。" ---- - -# Bash库文档 - -> 文档编写日期: 2026-03-20 - -本目录包含CFDesktop构建系统使用的Bash库文件。 - -## 库文件列表 - -| 文件 | 说明 | -|------|------| -| `lib_common.sh` | 日志输出、颜色定义、通用工具函数 | -| `lib_config.sh` | INI 配置文件解析 | -| `lib_build.sh` | 构建相关工具函数 | - -## 使用方式 - -所有库文件可以独立source使用,或被其他脚本引用。 - -### 基本加载方式 - -```bash -#!/bin/bash - -# 获取库目录 -SCRIPT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# 加载需要的模块 -source "$SCRIPT_LIB/lib_common.sh" -source "$SCRIPT_LIB/lib_config.sh" -source "$SCRIPT_LIB/lib_build.sh" -```text - -## 依赖关系 - -```text -lib_common.sh - ├── (被依赖) lib_config.sh - └── (被依赖) lib_build.sh -```text - -- **lib_common.sh** 是基础库,不依赖其他库 -- **lib_config.sh** 依赖 lib_common.sh(用于日志输出) -- **lib_build.sh** 依赖 lib_common.sh(会自动加载) - -## 模块详细文档 - -- [lib_common.sh](lib_common.sh.md) - 日志、颜色、通用工具 -- [lib_build.sh](lib_build.sh.md) - 构建相关函数 -- [lib_config.sh](lib_config.sh.md) - 配置文件处理 - -## 快速参考 - -### 日志输出 (lib_common.sh) - -```bash -log_info "信息消息" -log_success "成功消息" -log_warn "警告消息" -log_error "错误消息" -log_separator -log_progress 5 10 "处理中" -```text - -### 配置解析 (lib_config.sh) - -```bash -eval "$(get_ini_config config.ini)" -echo "$config_cmake_generator" - -value=$(get_ini_value config.ini "cmake" "generator") -has_ini_value config.ini "cmake" "generator" && echo "存在" -```text - -### 构建操作 (lib_build.sh) - -```bash -clean_build_dir "$BUILD_DIR" -run_cmake_configure "Ninja" "Release" "$SOURCE_DIR" "$BUILD_DIR" -run_cmake_build "$BUILD_DIR" "--all" $(get_parallel_job_count) -```text diff --git a/document/scripts/lib/bash/index.md b/document/scripts/lib/bash/index.md index 22d953dd0..87a10e38d 100644 --- a/document/scripts/lib/bash/index.md +++ b/document/scripts/lib/bash/index.md @@ -1,12 +1,71 @@ --- title: Bash 脚本库 -description: 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括 +description: 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括彩色日志输出、路径工具、环境变量检测与 CMake 封装调用等。 --- # Bash 脚本库 本目录包含 Bash 共享工具函数,为 Linux 平台的构建、测试与发布脚本提供通用功能支持,包括彩色日志输出、路径工具、环境变量检测与 CMake 封装调用等。 ---- +## 库文件列表 + +| 文件 | 说明 | +|------|------| +| `lib_common.sh` | 日志输出、颜色定义、通用工具函数 | +| `lib_config.sh` | INI 配置文件解析 | +| `lib_build.sh` | 构建相关工具函数 | + +## 使用方式 + +所有库文件可以独立 source 使用,或被其他脚本引用。 + +### 基本加载方式 + +```bash +#!/bin/bash + +# 获取库目录 +SCRIPT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 加载需要的模块 +source "$SCRIPT_LIB/lib_common.sh" +source "$SCRIPT_LIB/lib_config.sh" +source "$SCRIPT_LIB/lib_build.sh" +``` + +## 模块详细文档 + +- [lib_common.sh](lib_common.sh.md) - 日志、颜色、通用工具 +- [lib_build.sh](lib_build.sh.md) - 构建相关函数 +- [lib_config.sh](lib_config.sh.md) - 配置文件处理 + +## 快速参考 + +### 日志输出 (lib_common.sh) + +```bash +log_info "信息消息" +log_success "成功消息" +log_warn "警告消息" +log_error "错误消息" +log_separator +log_progress 5 10 "处理中" +``` + +### 配置解析 (lib_config.sh) + +```bash +eval "$(get_ini_config config.ini)" +echo "$config_cmake_generator" + +value=$(get_ini_value config.ini "cmake" "generator") +has_ini_value config.ini "cmake" "generator" && echo "存在" +``` + +### 构建操作 (lib_build.sh) -*Last updated: 2026-03-20* +```bash +clean_build_dir "$BUILD_DIR" +run_cmake_configure "Ninja" "Release" "$SOURCE_DIR" "$BUILD_DIR" +run_cmake_build "$BUILD_DIR" "--all" $(get_parallel_job_count) +``` diff --git a/document/scripts/lib/powershell/README.md b/document/scripts/lib/powershell/README.md deleted file mode 100644 index ea546aba5..000000000 --- a/document/scripts/lib/powershell/README.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: PowerShell库文档 -description: "文档编写日期: 2026-03-20,本目录包含 CFDesktop 构建系统使用的 PowerSh" ---- - -# PowerShell库文档 - -> 文档编写日期: 2026-03-20 - -本目录包含 CFDesktop 构建系统使用的 PowerShell 库模块文档。 - -## 模块列表 - -| 文件 | 说明 | -|------|------| -| LibCommon.psm1 | 日志、通用工具函数 | -| LibBuild.psm1 | 构建相关函数(CMake、目录管理) | -| LibConfig.psm1 | INI 配置文件解析 | -| LibGit.psm1 | Git 相关操作函数 | -| LibPaths.psm1 | 路径处理函数 | -| LibArgs.psm1 | 参数解析函数 | - -## 使用方式 - -### 单个模块加载 - -```powershell -Import-Module scripts/lib/powershell/LibCommon.psm1 -Import-Module scripts/lib/powershell/LibConfig.psm1 -```text - -### 脚本内加载(点号加载) - -```powershell -. "$PSScriptRoot\LibCommon.psm1" -. "$PSScriptRoot\LibConfig.psm1" -. "$PSScriptRoot\LibBuild.psm1" -```text - -## 依赖关系 - -```text -LibCommon.psm1 (基础模块,无依赖) - ├── LibBuild.psm1 (依赖 LibCommon) - ├── LibConfig.psm1 (无依赖) - ├── LibPaths.psm1 (无依赖) - ├── LibArgs.psm1 (无依赖) - └── LibGit.psm1 (无依赖) -```text - -**注意**: LibBuild.psm1 依赖 LibCommon.psm1,使用前必须先加载 LibCommon.psm1。 - -## 模块详细文档 - -- [LibCommon.psm1](LibCommon.psm1.md) - 日志输出功能 -- [LibBuild.psm1](LibBuild.psm1.md) - 构建工具函数 -- [LibConfig.psm1](LibConfig.psm1.md) - 配置文件解析 - -## 快速参考 - -### 日志输出 (LibCommon) - -```powershell -Write-LogInfo "Information message" -Write-LogSuccess "Operation completed" -Write-LogWarning "Warning message" -Write-LogError "Error message" -Write-LogSeparator -Write-LogProgress -Current 5 -Total 10 -Message "Processing" -```text - -### 构建操作 (LibBuild) - -```powershell -# 清理构建目录 -Clean-BuildDir "C:\Build\Output" - -# 确保目录存在 -Ensure-BuildDir "C:\Build\Output" - -# CMake 配置 -Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" - -# CMake 构建 -Invoke-CMakeBuild -BuildDir "build" -Parallel (Get-ParallelJobCount) -```text - -### 配置读取 (LibConfig) - -```powershell -# 读取完整配置 -$config = Get-IniConfig -FilePath "config.ini" -$value = $config["section"]["key"] - -# 读取单个值 -$value = Get-IniValue -FilePath "config.ini" -Section "section" -Key "key" - -# 检查配置存在 -if (Test-IniValue -FilePath "config.ini" -Section "section" -Key "key") { - # 配置存在 -} -```text diff --git a/document/scripts/lib/powershell/index.md b/document/scripts/lib/powershell/index.md index ab5a37c13..670f647ba 100644 --- a/document/scripts/lib/powershell/index.md +++ b/document/scripts/lib/powershell/index.md @@ -1,12 +1,87 @@ --- title: PowerShell 脚本库 -description: 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通 +description: 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通用功能支持,包括日志输出、路径处理、环境检测与 CMake 封装调用等,与 Bash 脚本库保持功能对齐。 --- # PowerShell 脚本库 本目录包含 PowerShell 共享工具函数,为 Windows 平台的构建、测试与发布脚本提供通用功能支持,包括日志输出、路径处理、环境检测与 CMake 封装调用等,与 Bash 脚本库保持功能对齐。 ---- +## 模块列表 + +| 文件 | 说明 | +|------|------| +| LibCommon.psm1 | 日志、通用工具函数 | +| LibBuild.psm1 | 构建相关函数(CMake、目录管理) | +| LibConfig.psm1 | INI 配置文件解析 | +| LibGit.psm1 | Git 相关操作函数 | +| LibPaths.psm1 | 路径处理函数 | +| LibArgs.psm1 | 参数解析函数 | + +## 使用方式 + +### 单个模块加载 + +```powershell +Import-Module scripts/lib/powershell/LibCommon.psm1 +Import-Module scripts/lib/powershell/LibConfig.psm1 +``` + +### 脚本内加载(点号加载) + +```powershell +. "$PSScriptRoot\LibCommon.psm1" +. "$PSScriptRoot\LibConfig.psm1" +. "$PSScriptRoot\LibBuild.psm1" +``` + +## 模块详细文档 + +- [LibCommon.psm1](LibCommon.psm1.md) - 日志输出功能 +- [LibBuild.psm1](LibBuild.psm1.md) - 构建工具函数 +- [LibConfig.psm1](LibConfig.psm1.md) - 配置文件解析 + +## 快速参考 + +### 日志输出 (LibCommon) + +```powershell +Write-LogInfo "Information message" +Write-LogSuccess "Operation completed" +Write-LogWarning "Warning message" +Write-LogError "Error message" +Write-LogSeparator +Write-LogProgress -Current 5 -Total 10 -Message "Processing" +``` + +### 构建操作 (LibBuild) + +```powershell +# 清理构建目录 +Clean-BuildDir "C:\Build\Output" + +# 确保目录存在 +Ensure-BuildDir "C:\Build\Output" + +# CMake 配置 +Invoke-CMakeConfigure -Generator "Ninja" -BuildType "Release" -SourceDir "." -BuildDir "build" + +# CMake 构建 +Invoke-CMakeBuild -BuildDir "build" -Parallel (Get-ParallelJobCount) +``` + +### 配置读取 (LibConfig) + +```powershell +# 读取完整配置 +$config = Get-IniConfig -FilePath "config.ini" +$value = $config["section"]["key"] + +# 读取单个值 +$value = Get-IniValue -FilePath "config.ini" -Section "section" -Key "key" -*Last updated: 2026-03-20* +# 检查配置存在 +if (Test-IniValue -FilePath "config.ini" -Section "section" -Key "key") { + # 配置存在 +} +``` diff --git a/document/scripts/release/hooks/README.md b/document/scripts/release/hooks/README.md deleted file mode 100644 index 7c84ebbfe..000000000 --- a/document/scripts/release/hooks/README.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Git Hooks -description: "文档编写日期: 2026-03-20,CFDesktop项目的Git hooks配置目录。" ---- - -# Git Hooks - -> 文档编写日期: 2026-03-20 - -CFDesktop项目的Git hooks配置目录。 - -## 快速安装 - -```bash -# Linux/macOS -bash scripts/release/hooks/install_hooks.sh - -# Windows PowerShell -.\scripts\release\hooks\install_hooks.ps1 -```bash - -## 文件说明 - -| 文件 | 说明 | -|------|------| -| pre-commit.sample | 代码格式检查钩子 | -| pre-push.sample | Docker构建验证钩子 | -| version_utils.sh | 版本号解析辅助函数 | -| install_hooks.sh | Linux/macOS安装脚本 | -| install_hooks.ps1 | Windows安装脚本 | - -## 验证级别 - -- **main分支**: X64 FastBuild + Tests -- **release分支**: 根据Major/Minor/Patch自动检测 -- **feat分支**: 跳过pre-push验证 - -## 详细文档 - -完整使用指南请参考: document/release_rule/git_hooks_guide.md diff --git a/document/scripts/release/hooks/index.md b/document/scripts/release/hooks/index.md index d59853b91..96cec5663 100644 --- a/document/scripts/release/hooks/index.md +++ b/document/scripts/release/hooks/index.md @@ -1,12 +1,38 @@ --- title: Git Hooks -description: 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 等关键操作前后自动执行代码检查、文档 +description: 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 `git push` 等关键操作前后自动执行代码检查、文档生成和版本一致性验证,防止不符合规范的代码进入远程仓库。 --- # Git Hooks 本目录包含发布流程中使用的 Git Hooks 脚本,用于在 `git push` 等关键操作前后自动执行代码检查、文档生成和版本一致性验证,防止不符合规范的代码进入远程仓库。 ---- +## 快速安装 + +```bash +# Linux/macOS +bash scripts/release/hooks/install_hooks.sh + +# Windows PowerShell +.\scripts\release\hooks\install_hooks.ps1 +``` + +## 文件说明 + +| 文件 | 说明 | +|------|------| +| pre-commit.sample | 代码格式检查钩子 | +| pre-push.sample | Docker构建验证钩子 | +| version_utils.sh | 版本号解析辅助函数 | +| install_hooks.sh | Linux/macOS安装脚本 | +| install_hooks.ps1 | Windows安装脚本 | + +## 验证级别 + +- **main分支**: X64 FastBuild + Tests +- **release分支**: 根据Major/Minor/Patch自动检测 +- **feat分支**: 跳过pre-push验证 + +## 详细文档 -*Last updated: 2026-03-20* +完整使用指南请参考: document/release_rule/git_hooks_guide.md diff --git a/document/todo/README.md b/document/todo/README.md deleted file mode 100644 index a2b50237f..000000000 --- a/document/todo/README.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: CFDesktop 项目 TODO 看板 -description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 设计文档和 架构规范整理 ---- - -# CFDesktop 项目 TODO 看板 - -## 概述 - -本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 - -## 模块索引 - -| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | -|----------|------|---------|------|------| -| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | -| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | -| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | -| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | -| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | -| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | -| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | -| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | - -## 状态图例 - -- ⬜ **待开始** (Todo) - 尚未开始的任务 -- 🚧 **进行中** (In Progress) - 正在开发的任务 -- ✅ **已完成** (Done) - 已完成的任务 -- ⚠️ **已废弃** (Deprecated) - 不再需要的任务 -- 🔄 **阻塞中** (Blocked) - 被依赖阻塞的任务 - -## 里程碑时间线 - -| 里程碑 | 时间 | 交付物 | -|--------|------|--------| -| M0 | Week 2 | ✅ 工程骨架 + Git Hooks CI/CD | -| M1 | Week 5 | 硬件探针 + 三档能力分级 | -| M2 | Week 9 | Base 库 + 主题引擎 + 输入抽象 | -| M3 | Week 15 | Shell UI 主体可用 | -| M4 | Week 18 | SDK 导出 + 示例应用 | -| M5 | Week 21 | 模拟器可用 | -| M6 | Week 23 | 应用商店基础 + 完整 CI/CD | - -## 快速链接 - -### 按角色查找 - -- **新手入门**: 从 [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) 开始 -- **基础开发**: [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) -- **UI 开发**: [99_ui_material_framework.md](base/99_ui_material_framework.md) + [02_input_layer.md](base/02_input_layer.md) -- **调试工具**: [03_simulator.md](base/03_simulator.md) -- **测试工程师**: [04_testing.md](base/04_testing.md) - -### 按任务类型查找 - -- **架构设计**: 各模块文档中的"架构设计"章节 -- **API 接口**: 各模块文档中的"类接口设计"章节 -- **单元测试**: [04_testing.md](base/04_testing.md) + 各模块文档中的"单元测试"章节 -- **性能优化**: 各模块文档中的"性能要求"章节 - -## 文档同步 - -本 TODO 目录与以下文档保持同步: -- `../design_stage/` - 详细设计文档 -- `../../ui/MaterialRules.md` - UI Material 架构规范 -- `../../BLUEPRINT.md` - 项目整体规划 - -## 更新记录 - -| 日期 | 变更 | 影响模块 | -|------|------|----------| -| 2026-03-30 | 更新 v0.13.1 进度: WSL X11 Backend Ready, Windows Desktop Backend, 显示后端架构完成 | desktop/, done/ | -| 2026-03-18 | Base库完成,删除02_base_library.md,重新编号 | base/ | -| 2026-03-07 | CI/CD 完成 (Git Hooks 策略) | 工程骨架 | -| 2026-03-05 | 创建 TODO 看板 | 全部 | - ---- - -*最后更新: 2026-03-30* diff --git a/document/todo/done/00_project_skeleton_status.md b/document/todo/done/00_project_skeleton_status.md deleted file mode 100644 index ff55a7eb8..000000000 --- a/document/todo/done/00_project_skeleton_status.md +++ /dev/null @@ -1,447 +0,0 @@ ---- -title: "Phase 0: 工程骨架搭建 - 状态文档" -description: "模块ID: Phase 0,总体进度: 100%" ---- - -# Phase 0: 工程骨架搭建 - 状态文档 - -> **模块ID**: Phase 0 -> **状态**: ✅ 已完成 -> **总体进度**: 100% -> **最后更新**: 2026-03-11 - ---- - -## 一、模块概述 - -工程骨架模块是 CFDesktop 项目的基础设施层,负责提供完整的构建系统、开发环境配置和 CI/CD 流水线。该模块为所有后续开发工作提供稳固的工程化基础。 - -### 核心职责 - -1. **CMake 构建系统** - 跨平台编译、依赖管理、输出配置 -2. **代码规范配置** - 格式化、静态分析、编码标准 -3. **开发工具集成** - IDE 配置、调试支持、辅助脚本 -4. **CI/CD 流水线** - 自动构建、文档部署、质量检查 - ---- - -## 二、完成状态总览 - -| 项目 | 完成度 | 状态 | -|------|--------|------| -| CMake 构建系统 | 90% | ✅ | -| 代码规范配置 | 80% | ✅ | -| 开发工具配置 | 70% | ✅ | -| CI/CD 流水线 | 100% | ✅ | -| 交叉编译支持 | 0% | ⚠️ 已取消 (Docker替代) | - ---- - -## 三、已完成工作 - -### 3.1 CMake 构建系统 (90%) - -**主配置文件**: `CMakeLists.txt` - -**已完成功能**: -- CMake 3.16+ 版本要求 -- C++17 标准设置 -- Qt6 集成 (Core, Gui, Widgets) -- 自动化 MOC/RCC/UIC 处理 -- 构建日志辅助系统 -- 编译命令导出 (compile_commands.json) -- 分类输出目录配置 (bin/, lib/, examples/) -- VSCode clangd 配置自动生成 -- VSCode 调试配置自动生成 - -**关键配置摘要**: -```cmake -cmake_minimum_required(VERSION 3.16) -project(CFDesktop VERSION 0.0.1 LANGUAGES CXX) - -# C++17 标准 -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Qt6 依赖 -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) - -# 子模块 -add_subdirectory(base) # 基础库 -add_subdirectory(ui) # UI 框架 -add_subdirectory(example) # 示例程序 -add_subdirectory(test) # 测试代码 -```text - -**依赖文件**: -- `cmake/build_log_helper.cmake` - 构建日志辅助 -- `cmake/check_toolchain.cmake` - 工具链检查 -- `cmake/custom_target_helper.cmake` - 自定义目标辅助 -- `cmake/OutputDirectoryConfig.cmake` - 输出目录配置 -- `cmake/generate_develop_helpers.cmake` - 开发辅助生成 -- `cmake/ExampleLauncher.cmake` - Windows 启动脚本生成 -- `cmake/QtDeployUtils.cmake` - Qt 部署工具 - -### 3.2 代码规范配置 (80%) - -**配置文件**: `.clang-format` - -**已完成功能**: -- 基于 LLVM 风格 -- 4 空格缩进 -- 100 字符列宽 -- 左对齐指针/引用 (int* a) -- 大括号附加风格 (Attach) -- 包含块排序 -- C++17 标准 - -**配置摘要**: -```yaml -BasedOnStyle: LLVM -IndentWidth: 4 -UseTab: Never -ColumnLimit: 100 -PointerAlignment: Left -BreakBeforeBraces: Attach -Standard: c++17 -SortIncludes: true -```text - -### 3.3 开发工具集成 (70%) - -**VSCode 配置**: `.vscode/settings.json` - -**已完成功能**: -- clangd 路径配置 -- 编译命令目录指定 -- 查询驱动器配置 -- 后台索引启用 -- clang-tidy 集成 -- 头文件插入优化 - -**其他 VSCode 配置**: -- `.vscode/extensions.json` - 推荐扩展 -- `.vscode/launch.json` - 调试配置 (自动生成) - -### 3.4 CI/CD 流水线 (100%) - -**策略**: Git Hooks 本地验证,无需远程 CI 构建流水线 - -#### 3.4.1 Git Pre-Push Hook - -**文件**: `scripts/release/hooks/pre-push.sample` - -**已完成功能**: -- 版本号检查 (阻止未更新版本的推送) -- Docker 构建验证 (本地执行) -- main 分支: X64 FastBuild + Tests -- release 分支: 根据 Major/Minor/Patch 自动检测验证级别 - - Major: X64 + ARM64 完整构建 - - Minor: X64 完整构建 - - Patch: X64 FastBuild + Tests - -#### 3.4.2 Git Pre-Commit Hook - -**文件**: `scripts/release/hooks/pre-commit.sample` - -**已完成功能**: -- 空白字符检查 (trailing whitespace) -- C++ 代码自动格式化 (clang-format) -- 支持跨平台 (Windows PowerShell / Linux bash) - -#### 3.4.3 Docker 构建系统 - -**文件**: `scripts/docker/Dockerfile.build` - -**已完成功能**: -- 多架构支持 (amd64/arm64/armhf) -- Qt 6.8.1 通过 aqtinstall 自动安装 -- 依赖自动化安装 (`scripts/dependency/install_build_dependencies.sh`) - -#### 3.4.4 Docker 构建脚本 - -**文件**: `scripts/build_helpers/docker_start.sh` - -**已完成功能**: -- 多架构构建 (--arch amd64/arm64) -- CI 验证模式 (--verify) -- 快速构建 (--fast-build) -- 项目构建 (--build-project) -- 测试运行 (--run-project-test) -- 美化日志输出 - -#### 3.4.5 CI 构建入口 - -**文件**: `scripts/build_helpers/ci_build_entry.sh` - -**已完成功能**: -- 自动架构检测 (x86_64/aarch64/armv7l) -- 自动选择对应配置文件 - -#### 3.4.6 版本工具 - -**文件**: `scripts/release/hooks/version_utils.sh` - -**已完成功能**: -- 版本号解析 (Major/Minor/Patch) -- 验证级别自动检测 -- 本地/远程版本比较 - -#### 3.4.7 Hooks 安装脚本 - -**文件**: `scripts/release/hooks/install_hooks.sh` - -**已完成功能**: -- 自动安装 pre-commit 和 pre-push hooks -- 支持交互式确认 -- 备份现有 hooks - -#### 3.4.8 文档部署流水线 - -**文件**: `.github/workflows/deploy.yml` - -**已完成功能**: -- MkDocs 文档自动构建 -- Doxygen API 文档生成 -- doxybook2 Markdown 转换 -- GitHub Pages 自动部署 -- Python 3.11 环境配置 - -**触发条件**: -- Push 到 main 分支 -- 手动触发 (workflow_dispatch) - -### 3.5 目录结构 - -**已建立的目录结构**: -```text -CFDesktop/ -├── base/ # 基础库模块 -│ ├── system/ # 系统检测 (CPU, 内存) -│ └── include/ # 公共头文件 -├── ui/ # UI 框架 -│ ├── core/ # 核心组件 -│ ├── material/ # Material Design -│ └── base/ # 基础工具 -├── example/ # 示例程序 -├── test/ # 测试代码 -├── cmake/ # CMake 模块 -├── .github/ # GitHub 配置 -└── .vscode/ # VSCode 配置 -```bash - ---- - -## 四、实施时间线 - -### Week 1 任务 - -#### Day 1-2: 目录结构创建 -- [x] 创建完整目录树 - - [x] `base/` - 基础库源码 (system/, utilities/) - - [x] `ui/` - UI 框架源码 (core/, components/, widget/) - - [x] `example/` - 示例程序 - - [x] `test/` - 测试代码 - - [x] `cmake/` - CMake 模块 - - [x] `scripts/` - 构建和辅助脚本 -- [x] 编写主 `CMakeLists.txt` - - [x] 设置 C++17 标准 - - [x] 配置 Qt6 依赖 - - [x] 添加子目录 - - [x] 自动化 MOC/RCC/UIC -- [x] 创建各子模块的 CMakeLists.txt 框架 -- [x] 配置 VSCode 开发环境 - - [x] `.vscode/settings.json` - clangd 配置 - - [x] `.vscode/extensions.json` - 推荐扩展 - - [x] `.vscode/launch.json` - 调试配置 - -#### Day 3-4: ~~交叉编译配置~~ (已取消,使用 Docker 多架构替代) -- ~~编写 ARMv7 工具链文件~~ -- ~~编写 ARM64 工具链文件~~ -- [x] Docker 多架构构建替代方案 - - [x] `scripts/docker/Dockerfile.build` - 支持 amd64/arm64/armhf - -#### Day 5: 代码规范配置 -- [x] 配置 `.clang-format` - - [x] 基于 LLVM 风格 - - [x] 设置缩进为 4 空格 - - [x] 配置列宽 100 - - [x] 设置指针对齐方式 -- [x] 配置 Git pre-commit hook - - [x] 创建 `scripts/release/hooks/pre-commit.sample` 脚本 - - [x] 自动格式化 (clang-format) - - [x] 添加 trailing whitespace 检查 - - [x] 配置可跳过选项 (git commit --no-verify) - - [x] 安装脚本配置 (`scripts/release/hooks/install_hooks.sh`) - -### Week 2 任务 - -#### Day 1-3: CI/CD 搭建 -- [x] 创建 Docker 构建镜像 - - [x] `scripts/docker/Dockerfile.build` - 多架构支持 (amd64/arm64/armhf) - - [x] 配置 Qt6 环境 (aqtinstall 6.8.1) -- [x] 编写 Git Hooks 工作流 - - [x] `scripts/release/hooks/pre-push.sample` - Push 前验证 - - [x] `scripts/release/hooks/pre-commit.sample` - Commit 前检查 - - [x] `scripts/release/hooks/version_utils.sh` - 版本工具 - - [x] `scripts/release/hooks/install_hooks.sh` - Hooks 安装脚本 -- [x] Docker 构建脚本 - - [x] `scripts/build_helpers/docker_start.sh` - 本地 Docker 构建 - - [x] `scripts/build_helpers/ci_build_entry.sh` - CI 入口 (自动架构检测) -- [x] 配置部署流程 `.github/workflows/deploy.yml` - MkDocs 文档自动部署 -- [x] 测试完整构建流程 - -#### Day 4-5: 开发工具完善 -- [x] 创建 VSCode 工作区配置 `.vscode/settings.json` - - [x] CMake 配置参数 - - [x] Clangd 配置 -- [x] 配置推荐扩展 `.vscode/extensions.json` - - [x] CMake Tools - - [x] C/C++ - - [x] clang-format -- [x] 编写 Hello World 测试程序 - - [x] `example/base/system/` - 系统信息示例 - - [x] `example/ui/widget/material/` - 控件示例 -- [x] 编写开发环境设置文档 - - [x] `document/development/01_prerequisites.md` - 前置要求 - - [x] `document/development/02_quick_start.md` - 快速开始 - - [x] `document/development/03_build_system.md` - 构建系统 - - [x] `document/development/04_development_tools.md` - 开发工具 - - [x] `document/development/05_docker_build.md` - Docker 构建 - - [x] `document/development/06_git_hooks.md` - Git Hooks - - [x] `document/development/07_troubleshooting.md` - 常见问题 - ---- - -## 五、重要架构决策 - -### 5.1 CMakePresets.json - 已取消 - -**原因**: -- 不方便维护 -- 配置与命令行参数重复 -- 团队更习惯使用 -DCMAKE_* 参数 - -**替代方案**: -- 使用 build_*.config.ini 配置文件 -- 通过命令行参数传递构建配置 - -### 5.2 src/base/sdk/shell 三层结构 - 已调整 - -**原计划**: -```text -src/ -├── base/ # 基础库 -├── sdk/ # SDK 层 -└── shell/ # Shell UI -```text - -**实际采用**: -```text -base/ # 基础库 -ui/ # UI 框架 -example/ # 示例程序 -```yaml - -**原因**: 更简洁的模块划分 - -### 5.3 ARM 交叉编译工具链 - 已推迟 - -**原因**: 优先实现桌面平台功能,嵌入式平台后续支持 - ---- - -## 六、验收标准 - -### CI/CD -- [x] Push 代码前自动触发本地构建验证 (pre-push hook) -- [x] Commit 前自动代码格式检查 (pre-commit hook) -- [x] 版本号检查 (阻止未更新版本的推送) -- [x] 多架构构建支持 (amd64/arm64/armhf) -- [x] 代码格式化一键执行 (pre-commit hook 自动格式化) -- [x] MkDocs 文档自动部署到 gh-pages - -### 开发环境 -- [x] VSCode 能够正确索引所有符号 (clangd 配置) -- [x] 代码格式化一键执行 (pre-commit hook) -- [x] 调试配置可用 (`.vscode/launch.json` 自动生成) -- [x] 新团队成员能在 30 分钟内完成环境搭建 (`document/development/`) - ---- - -## 七、关键文件路径 - -### 已实现文件 - -```text -CFDesktop/ -├── CMakeLists.txt # 主 CMake 配置 -├── .clang-format # 代码格式化配置 -├── .vscode/ -│ ├── settings.json # VSCode 设置 -│ ├── extensions.json # 推荐扩展 -│ └── launch.json # 调试配置 (自动生成) -├── .github/ -│ └── workflows/ -│ └── deploy.yml # 文档部署流水线 -├── scripts/ -│ ├── docker/ -│ │ └── Dockerfile.build # 多架构 Docker 镜像 -│ ├── build_helpers/ -│ │ ├── docker_start.sh # Docker 构建脚本 -│ │ ├── ci_build_entry.sh # CI 构建入口 -│ │ └── build_ci_*.ini # 多架构配置 -│ ├── release/ -│ │ └── hooks/ -│ │ ├── pre-commit.sample # Pre-commit hook -│ │ ├── pre-push.sample # Pre-push hook -│ │ ├── version_utils.sh # 版本工具 -│ │ └── install_hooks.sh # Hooks 安装脚本 -│ └── dependency/ -│ └── install_build_dependencies.sh # 依赖安装 -└── cmake/ - ├── build_log_helper.cmake # 构建日志 - ├── check_toolchain.cmake # 工具链检查 - ├── custom_target_helper.cmake # 自定义目标 - ├── OutputDirectoryConfig.cmake # 输出配置 - ├── generate_develop_helpers.cmake # 开发辅助生成 - ├── ExampleLauncher.cmake # 启动脚本生成 - └── QtDeployUtils.cmake # Qt 部署工具 -```text - -### 待创建文件 - -```text -CFDesktop/ -├── .clang-tidy # 静态分析配置 (不考虑) -├── .github/ -│ └── workflows/ -│ └── build.yml # 构建流水线 (不需要,Git Hooks 替代) -└── cmake/ - └── toolchains/ - ├── arm-linux-gnueabihf.cmake # ARMv7 工具链 (推迟) - └── aarch64-linux-gnu.cmake # ARM64 工具链 (推迟) -```bash - ---- - -## 八、风险与缓解 - -| 风险 | 影响 | 缓解措施 | -|------|------|----------| -| Docker 多架构构建时间较长 | 开发效率 | 使用 --fast-build 缓存镜像 | -| ARM64 QEMU 仿真速度慢 | CI 耗时 | 仅在 release/Major 版本验证 | -| 工具链版本兼容性 | 编译失败 | 固定 Docker 镜像版本 | - ---- - -## 九、相关文档 - -- 原始TODO: ~~已归档~~(原 `00_project_skeleton.md` 已删除) -- 设计文档: `../../design_stage/00_phase0_project_skeleton.md` -- 开发环境设置: `../../development/` - ---- - -*文档版本: v1.0* -*生成时间: 2026-03-11* diff --git a/document/todo/done/01_hardware_probe_status.md b/document/todo/done/01_hardware_probe_status.md deleted file mode 100644 index 7189c1436..000000000 --- a/document/todo/done/01_hardware_probe_status.md +++ /dev/null @@ -1,435 +0,0 @@ ---- -title: "Phase 1: 硬件探针与能力分级 - 状态文档" -description: "模块ID: Phase 1,状态: 🚧 部分完成" ---- - -# Phase 1: 硬件探针与能力分级 - 状态文档 - -> **模块ID**: Phase 1 -> **状态**: ✅ 完成 -> **总体进度**: 100% -> **最后更新**: 2026-05-23 - ---- - -## 一、模块概述 - -硬件探针模块是 CFDesktop 项目的能力分级核心,负责在启动时自动检测系统硬件能力,根据检测结果计算硬件档位 (HWTier),并为上层模块提供对应的能力策略配置。 - -### 核心职责 - -1. **硬件检测** - CPU、GPU、内存、网络自动检测 -2. **能力分级** - 根据检测结果计算 Low/Mid/High 三档 -3. **策略引擎** - 为各模块提供对应档位的能力配置 -4. **配置覆盖** - 支持手动配置和自定义检测脚本 - -### 档位定义 - -| 档位 | 典型硬件 | 动画 | 渲染 | 视频解码 | 内存限制 | -|------|----------|------|------|----------|----------| -| Low | IMX6ULL (528MHz Cortex-A7) | 禁用 | linuxfb | 软件 | < 64MB | -| Mid | RK3568 (4xCortex-A55, Mali-G52) | 部分启用 | eglfs可选 | H.264/H.265 部分 | < 256MB | -| High | RK3588 (8xCortex-A76/A55, Mali-G610) | 全部启用 | eglfs+OpenGL ES 3.2+ | 全格式硬件解码 | < 1GB | - ---- - -## 二、完成状态总览 - -| 模块 | 完成度 | 状态 | -|------|--------|------| -| CPUDetector | 100% | ✅ 完成 | -| MemoryDetector | 95% | ✅ 完成 | -| GPUDetector | 90% | ✅ 核心功能已完成 | -| NetworkDetector | 85% | ✅ 核心功能已完成 | -| HWTier 系统 | 100% | ✅ 完成 | -| CapabilityPolicy | 100% | ✅ 完成 | -| HardwareProbe 主类 | 100% | ✅ 完成 | - ---- - -## 三、已完成工作 - -### 3.1 CPU 检测器 (80%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/cpu/cfcpu.h` | CPU 基础信息接口 | -| `base/include/system/cpu/cfcpu_profile.h` | CPU 详细信息 (频率、核心数) | -| `base/include/system/cpu/cfcpu_bonus.h` | CPU 扩展信息 | -| `base/system/cpu/cfcpu.cpp` | 基础信息实现 | -| `base/system/cpu/cfcpu_profile.cpp` | 详细信息实现 | -| `base/system/cpu/cfcpu_bonus.cpp` | 扩展信息实现 | -| `base/system/cpu/private/linux_impl/*.cpp` | Linux 平台实现 | -| `base/system/cpu/private/win_impl/*.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] `CPUInfoView` 结构体 - 型号、架构、厂商 -- [x] `/proc/cpuinfo` 解析 (Linux) -- [x] WMI 查询 (Windows) -- [x] CPU 核心数检测 -- [x] CPU 频率检测 -- [x] CPU 特性检测 (neon, vfpv4, AVX, etc.) -- [x] 设备树 compatible 读取 -- [x] `uname` 架构检测 - -#### 测试 - -- 测试文件: `test/system/test_cpu_info_query.cpp` - ---- - -### 3.2 内存检测器 (80%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/memory/memory_info.h` | 内存信息接口 | -| `base/system/memory/memory_info.cpp` | 内存信息实现 | -| `base/system/memory/private/linux_impl/*.cpp` | Linux 平台实现 | -| `base/system/memory/private/win_impl/*.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] 总内存检测 -- [x] 可用内存检测 -- [x] Swap 内存检测 -- [x] 物理内存详情 (DIMM) -- [x] 进程内存统计 -- [x] 缓存内存统计 - -#### 测试 - -- 测试文件: `test/system/test_memory_info_query.cpp` - ---- - -### 3.3 GPU 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/gpu/gpu.h` | GPU/显示信息接口 | -| `base/system/gpu/gpu.cpp` | 跨平台实现 | -| `base/system/gpu/private/linux_impl/gpu_info.cpp` | Linux 平台实现 | -| `base/system/gpu/private/win_impl/gpu_info.cpp` | Windows 平台实现 | - -#### 功能实现 - -- [x] `GPUInfo` 结构体 - GPU 设备信息 -- [x] `DisplayInfo` 结构体 - 显示器信息 -- [x] `EnvironmentScore` 结构体 - 环境评分 -- [x] Linux DRM 设备检测 (`/dev/dri/card*`) -- [x] Linux DeviceTree SoC 检测 -- [x] WSL2 GPU 探测 (`/dev/dxg`) -- [x] Windows DXGI 检测 -- [x] GPU 评分算法 (满分 50) -- [x] 显示评分算法 (满分 50) -- [x] 档位判定 (Low/Mid/High) - -#### 示例 - -- 示例文件: `example/base/system/example_gpu_info.cpp` - ---- - -### 3.4 网络检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/network/network.h` | 网络信息接口 | -| `base/system/network/network.cpp` | 基于 Qt 的跨平台实现 | - -#### 功能实现 - -- [x] `IpAddress` 结构体 - IPv4/IPv6 地址 -- [x] `InterfaceInfo` 结构体 - 网卡信息 -- [x] `AddressEntry` 结构体 - IP 地址条目 -- [x] `InterfaceFlags` 结构体 - 接口标志 -- [x] `NetworkStatus` 结构体 - 网络状态 -- [x] `Reachability` 枚举 - 网络可达性 -- [x] `TransportMedium` 枚举 - 传输介质 -- [x] `DnsEligibility` 枚举 - DNS 可达性 -- [x] `getNetworkInfo()` 函数 - -#### 示例 - -- 示例文件: `example/base/system/example_network_info.cpp` - ---- - -## 四、使用示例 - -```cpp -#include -#include - -// 查询 CPU 信息 -auto cpu_info = cf::getCPUInfo(); -if (cpu_info) { - qDebug() << "CPU:" << cpu_info.value().model.data(); - qDebug() << "Arch:" << cpu_info.value().arch.data(); -} - -// 查询内存信息 -auto mem_info = cf::getMemoryInfo(); -if (mem_info) { - qDebug() << "Total:" << mem_info.value().totalBytes; - qDebug() << "Available:" << mem_info.value().availableBytes; -} -```yaml - ---- - -## 五、HWTier 系统 (Phase 1 核心功能) — ✅ 已完成 - -> **注意**: GPU 和 Network 检测器已完成,HWTier 系统已实现,Phase 1 核心功能全部完成。 - -### 5.1 HWTier 枚举定义 ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier_data.h` - -- [x] `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) -- [x] 档位字符串转换函数 `hardwareTierLevelToString()` -- [x] 各档位对应硬件配置说明 (i.MX6ULL / RK3568 / RK3588) - -### 5.2 硬件数据收集器 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_collector.h`, `base/system/hardware_tier/default/default_collector.cpp` - -- [x] `IHardwareCollector` 接口 — 可插拔收集器 -- [x] `HardwareData` 结构体 — CPU/GPU/Memory/Display 原始数据 -- [x] 默认收集器 — 整合 CPU/GPU/Memory/Display 检测器 -- [x] 跨平台支持 (Linux/Windows) - -### 5.3 评分引擎 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_scorer.h`, `base/system/hardware_tier/default/default_*_scorer.cpp` - -- [x] `IHardwareScorer` 接口 — 可插拔评分器 -- [x] 维度评分类型: `CpuScore`, `GpuScore`, `MemoryScore`, `DisplayScore` (各 0-100) -- [x] 默认评分器: CPU/GPU/Memory/Display 四维度独立评分 -- [x] 可通过 `registerScorer()` 替换各维度评分逻辑 - -### 5.4 档位评估器 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_assessor.h`, `base/system/hardware_tier/default/default_assessor.cpp` - -- [x] `IHardwareAssessor` 接口 — 可插拔评估器 -- [x] `HardwareTierAssessment` 结构体 — 四维度评分 + 总档位 + 覆盖信息 -- [x] 默认评估器 — 综合四维度分数计算档位 - -### 5.5 策略引擎 ✅ - -**实现位置**: `base/system/hardware_tier/hardware_tier_policy.h`, `base/system/hardware_tier/default/default_policy.cpp` - -- [x] `IHardwarePolicy` 接口 — 可插拔策略 -- [x] `HardwareTierCapabilities` 结构体 — 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) -- [x] 默认策略 — 根据 Low/Mid/High 档位映射能力标志 -- [x] 部分动画支持 (`enable_partial_animation`) - -### 5.6 DeviceConfig 覆盖 ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` - -- [x] `setDeviceConfigOverride()` — 手动强制档位 -- [x] `clearDeviceConfigOverride()` — 清除覆盖 -- [x] 覆盖时跳过收集/评分,直接使用指定档位 -- [x] 覆盖原因记录 (`override_reason`) - -### 5.7 管线注册 API ✅ - -**实现位置**: `base/include/system/hardware_tier/hardware_tier.h` - -- [x] `registerCollector()` — 注册自定义收集器 -- [x] `registerScorer()` — 注册自定义评分器 (按维度) -- [x] `registerAssessor()` — 注册自定义评估器 -- [x] `registerPolicy()` — 注册自定义策略 -- [x] `assessHardware()` — 执行完整管线 (缓存支持) -- [x] `getHardwareTierCapabilities()` — 查询能力标志 - -### 5.8 示例代码 ✅ - -**实现位置**: `example/base/system/example_hardware_tier.cpp` - -- [x] 完整的硬件分级评估示例 -- [x] 四维度评分展示 -- [x] 能力标志展示 - ---- - -## 六、测试设计 - -### 6.1 单元测试用例 - -**文件**: `test/hardware/test_hardware_probe.cpp` - -```cpp -class TestHardwareProbe : public QObject { - Q_OBJECT - -private slots: - // CPU 检测测试 - void testDetectCPU_IMX6ULL(); - void testDetectCPU_RK3568(); - void testDetectCPU_RK3588(); - - // GPU 检测测试 - void testDetectGPU_WithDRM(); - void testDetectGPU_NoDRM(); - void testDetectGPU_OpenGLContext(); - - // 内存检测测试 - void testDetectMemory_512MB(); - void testDetectMemory_1GB(); - void testDetectMemory_4GB(); - - // 档位计算测试 - void testCalculateTier_IMX6ULL_returnsLow(); - void testCalculateTier_RK3568_returnsMid(); - void testCalculateTier_RK3588_returnsHigh(); - - // 配置文件测试 - void testDeviceConfig_LoadDefault(); - void testDeviceConfig_OverrideTier(); - void testDeviceConfig_CustomScript(); - - // 策略引擎测试 - void testCapabilityPolicy_LowTier(); - void testCapabilityPolicy_MidTier(); - void testCapabilityPolicy_HighTier(); - void testCapabilityPolicy_AnimationDisabledOnLow(); - - // 边界情况 - void testEmptyProcFiles(); - void testMalformedConfig(); - void testTierOverride(); -}; -```text - -### 6.2 评分算法 - -```cpp -void HardwareProbe::calculateTier(HardwareInfo& info) { - HWTier calculatedTier = HWTier::Low; - int score = 0; - - // CPU 评分 - score += std::min(info.cpu.cores, 8) * 10; // 最多 80 分 - if (info.cpu.frequencyMHz > 1000) score += 20; - if (info.cpu.features.contains("neon")) score += 10; - - // GPU 评分 - if (info.gpu.hasHardwareAcceleration) score += 50; - if (!info.gpu.driverPath.isEmpty()) score += 20; - - // 内存评分 - int memoryMB = info.memory.totalBytes / (1024 * 1024); - if (memoryMB >= 512) score += 20; - if (memoryMB >= 1024) score += 20; - if (memoryMB >= 2048) score += 10; - - // 档位判定 - // Low: 0-60, Mid: 61-120, High: 121+ - if (score >= 121) { - calculatedTier = HWTier::High; - } else if (score >= 61) { - calculatedTier = HWTier::Mid; - } else { - calculatedTier = HWTier::Low; - } - - info.tier = calculatedTier; -} -```yaml - ---- - -## 七、关键文件路径 - -### 已实现文件 - -```text -base/ -├── system/ -│ ├── cpu/ -│ │ ├── cfcpu.h # CPU 检测接口 -│ │ ├── cfcpu_bonus.h # CPU 扩展信息 -│ │ ├── cfcpu_profile.h # CPU 性能分析 -│ │ └── private/ -│ │ ├── linux_impl/ # Linux 实现 -│ │ └── win_impl/ # Windows 实现 -│ └── memory/ -│ ├── memory_info.h # 内存检测接口 -│ └── private/ -│ ├── linux_impl/ # Linux 实现 -│ └── win_impl/ # Windows 实现 -└── include/ - ├── system/cpu/ # CPU 公共头文件 - └── system/memory/ # 内存公共头文件 -```text - -### 待创建文件 - -> HWTier 系统已全部实现,无待创建文件。 - -```text -base/ -├── include/system/hardware_tier/ -│ ├── hardware_tier.h # 公共 API (管线注册 + 评估入口) -│ └── hardware_tier_data.h # 数据结构 (枚举/评分/结果/能力标志) -├── system/hardware_tier/ -│ ├── hardware_tier_collector.h # IHardwareCollector 接口 -│ ├── hardware_tier_scorer.h # IHardwareScorer 接口 -│ ├── hardware_tier_assessor.h # IHardwareAssessor 接口 -│ ├── hardware_tier_policy.h # IHardwarePolicy 接口 -│ ├── default_factories.h # 默认工厂函数 -│ ├── hardware_tier.cpp # 管线实现 -│ └── default/ -│ ├── default_collector.cpp # 默认收集器 -│ ├── default_cpu_scorer.cpp # 默认 CPU 评分 -│ ├── default_gpu_scorer.cpp # 默认 GPU 评分 -│ ├── default_memory_scorer.cpp # 默认 Memory 评分 -│ ├── default_display_scorer.cpp # 默认 Display 评分 -│ ├── default_assessor.cpp # 默认评估器 -│ └── default_policy.cpp # 默认策略 -```text - ---- - -## 八、下一步行动建议 - -### 已完成 ✅ - -1. **HWTier 枚举定义** — ✅ 已实现 (`HardwareTierLevel`) -2. **HardwareProbe 管线** — ✅ 已实现 (可插拔 Collect→Score→Assess→Policy 四阶段管线) -3. **CapabilityPolicy 策略引擎** — ✅ 已实现 (`IHardwarePolicy` + 默认策略) -4. **DeviceConfig 覆盖** — ✅ 已实现 (`setDeviceConfigOverride()`) - -### 后续可选增强 - -1. **集成 ConfigStore 查询覆盖** — 从 ConfigStore 读取设备档位配置,自动调用 `setDeviceConfigOverride()` -2. **自定义检测脚本执行** — 支持外部脚本注入硬件数据 -3. **Mock 数据集** — 单元测试用模拟数据 -4. **性能测试** — 各评分器在不同硬件下的基准测试 - ---- - -## 九、相关文档 - -- 原始TODO: ~~已归档~~(原 `01_hardware_probe.md` 已删除) -- 设计文档: `../../../design_stage/01_phase1_hardware_probe/` -- CPU 实现: `../../../HandBook/api/system/cpu/overview/` -- 内存实现: `../../../HandBook/api/system/memory/memory_info/` - ---- - -*文档版本: v2.0* -*生成时间: 2026-03-11* -*最后更新: 2026-05-23 (HWTier 系统完成)* diff --git a/document/todo/done/02_base_library_status.md b/document/todo/done/02_base_library_status.md deleted file mode 100644 index 6ec37edf5..000000000 --- a/document/todo/done/02_base_library_status.md +++ /dev/null @@ -1,625 +0,0 @@ ---- -title: "Phase 2: 基础库核心 - 状态文档" -description: "模块ID: Phase 2,总体进度: 100%" ---- - -# Phase 2: 基础库核心 - 状态文档 - -> **模块ID**: Phase 2 -> **状态**: ✅ 完成 -> **总体进度**: 100% -> **最后更新**: 2026-03-18 -> **架构说明**: 原规划在 `base/` 目录,实际实现在 `ui/` 和 `desktop/base/` 目录 - ---- - -## 一、模块概述 - -Base库核心是CFDesktop项目的基础设施层,为所有上层模块提供统一的主题管理、动画控制、分辨率适配、配置存储和日志记录能力。 - -### 核心职责 - -1. **主题管理** - 动态主题切换、变量解析、QSS处理 -2. **动画管理** - 动画生命周期、性能降级、预定义动画 -3. **分辨率适配** - dp/sp单位转换、屏幕参数检测 -4. **配置中心** - 层级存储、变更监听、持久化 -5. **日志系统** - 多Sink支持、日志轮转、标签过滤 - ---- - -## 二、完成状态总览 - -| 模块 | 完成度 | 实现位置 | 备注 | -|------|--------|----------|------| -| ThemeEngine | 60% | `ui/core/` | 核心框架已实现 | -| AnimationManager | 65% | `ui/components/` | 接口已定义 | -| DPI management | 80% | `ui/base/` | 基础功能完成 | -| ConfigStore | 100% | `desktop/base/config_manager/` | ✅ 完成 | -| Logger | 100% | `desktop/base/logger/` | ✅ 完成 | - ---- - -## 三、已完成工作 - -### 3.1 主题引擎 ThemeEngine (60%) - -**实际实现位置**: `ui/core/` (非原计划的 `base/`) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/core/theme.h` | ICFTheme 接口 | -| `ui/core/theme_manager.h` | 主题管理器 (单例) | -| `ui/core/theme_factory.h` | 主题工厂接口 | -| `ui/core/material/cfmaterial_theme.h` | Material 主题实现 | -| `ui/core/material/cfmaterial_scheme.h` | Material 配色方案 | -| `ui/core/material/cfmaterial_radius_scale.h` | 圆角规范 | -| `ui/core/material/cfmaterial_fonttype.h` | 字体规范 | -| `ui/core/material/cfmaterial_motion.h` | 动画规范 | -| `ui/core/material/material_factory_class.h` | Material 工厂 | - -#### 功能实现 - -- [x] 主题管理器 - `ThemeManager` 单例 -- [x] 主题注册/卸载 - `insert_one()`, `remove_one()` -- [x] 主题切换 - `setThemeTo()` -- [x] 主题变更信号 - `themeChanged` -- [x] Widget 主题安装 - `install_widget()` -- [x] Material 配色方案 - MaterialColorScheme -- [x] Token 系统 - `cfmaterial_token_literals.h` -- [x] 工厂模式创建 - MaterialFactory - -#### 接口摘要 - -```cpp -namespace cf::ui::core { -class ThemeManager : public QObject { -public: - static ThemeManager& instance(); - const ICFTheme& theme(const std::string& name) const; - bool insert_one(const std::string& name, InstallerMaker make_one); - void remove_one(const std::string& name); - void install_widget(QWidget* w); - void remove_widget(QWidget* w); - void setThemeTo(const std::string& name, bool doBroadcast = true); - const std::string& currentThemeName() const; -signals: - void themeChanged(const ICFTheme& new_theme); -}; -} -```bash - -#### 测试 - -- [x] `test/ui/core/token_test.cpp` - -#### 示例 - ---- - -### 3.2 动画管理器 AnimationManager (65%) - -**实际实现位置**: `ui/components/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/components/animation.h` | 动画基类 | -| `ui/components/animation_factory_manager.h` | 动画工厂管理器 | -| `ui/components/animation_group.h` | 动画组 | -| `ui/components/timing_animation.h` | 时序动画 | -| `ui/components/spring_animation.h` | 弹簧动画 | -| `ui/components/material/cfmaterial_animation_strategy.h` | 动画策略 | -| `ui/components/material/cfmaterial_animation_factory.h` | Material 动画工厂 | -| `ui/components/material/cfmaterial_fade_animation.h` | 淡入淡出 | -| `ui/components/material/cfmaterial_slide_animation.h` | 滑动动画 | -| `ui/components/material/cfmaterial_scale_animation.h` | 缩放动画 | -| `ui/components/material/cfmaterial_property_animation.h` | 属性动画 | - -#### 功能实现 - -- [x] 动画工厂管理器 -- [x] 动画注册/创建 -- [x] 预定义动画: - - [x] FadeAnimation (淡入淡出) - - [x] SlideAnimation (滑动,支持方向) - - [x] ScaleAnimation (缩放) -- [x] 动画组: - - [x] 并行组 - - [x] 串行组 -- [x] 动画生命周期管理 -- [x] 动画策略模式 -- [x] WeakPtr 所有权管理 - -#### 核心类型 - -```cpp -namespace cf::ui::components { -// 动画创建器函数类型 -using AnimationCreator = std::function; - -// 动画工厂管理器接口 -class ICFAnimationManagerFactory : public QObject { - virtual RegisteredResult registerOneAnimation(const QString& name, const QString& type) = 0; - virtual RegisteredResult registerAnimationCreator(const QString& name, AnimationCreator creator) = 0; - virtual cf::WeakPtr getAnimation(const char* name) = 0; - virtual void setEnabledAll(bool enabled) = 0; - virtual bool isAllEnabled() = 0; -}; - -// 动画状态枚举 -enum class State { Idle, Running, Paused, Finished }; -enum class Direction { Forward, Backward }; -} -```bash - -#### 示例 - ---- - -### 3.3 DPI 适配 (80%) - -**实际实现位置**: `ui/base/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/base/device_pixel.h` | dp/sp 转换工具 | -| `ui/base/device_pixel.cpp` | 实现 | - -#### 功能实现 - -- [x] `CanvasUnitHelper` 结构体 -- [x] `dpToPx()` - 设备无关像素转物理像素 -- [x] `spToPx()` - 可缩放像素转物理像素 -- [x] `pxToDp()` - 物理像素转设备无关像素 -- [x] `dpi()` - 获取设备像素比 -- [x] `BreakPoint` - 响应式断点 (Compact/Medium/Expanded) - -#### 接口摘要 - -```cpp -namespace cf::ui::base::device { -struct CanvasUnitHelper { - CanvasUnitHelper(const qreal devicePixelRatio); - qreal dpToPx(qreal dp) const; - qreal spToPx(qreal sp) const; - qreal pxToDp(qreal px) const; - qreal dpi() const; - - enum class BreakPoint { Compact, Medium, Expanded }; - BreakPoint breakPoint(qreal widthDp); -}; -} -```bash - -#### 测试 - -- [x] `test/ui/base/device_pixel_test.cpp` - ---- - -### 3.4 颜色系统支持 - -**位置**: `ui/base/color.h`, `ui/base/color_helper.h` - -**已完成**: -- HCT颜色空间支持 `CFColor` 类 -- 颜色混合 `blend()` -- 对比度计算 `contrastRatio()` -- 色调板生成 `tonalPalette()` -- 高程叠加 `elevationOverlay()` -- 相对亮度计算 `relativeLuminance()` - ---- - -### 3.5 ConfigStore - 配置中心 (100%) - -**实现位置**: `desktop/base/config_manager/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/config_manager/include/cfconfig_key.h` | 配置键定义 | -| `desktop/base/config_manager/include/cfconfig_layer.h` | 配置层级 (Temp/App/User/System) | -| `desktop/base/config_manager/include/cfconfig_notify_policy.h` | 通知策略 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_result.h` | 操作结果 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_watcher.h` | 配置监听器 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h` | 路径提供者 | -| `desktop/base/config_manager/src/cfconfig.cpp` | 实现 | -| `desktop/base/config_manager/src/impl/config_impl.h` | 内部实现 | -| `desktop/base/config_manager/src/impl/config_impl.cpp` | 内部实现 | - -#### 功能实现 - -- [x] 四层存储 (Temp/App/User/System) -- [x] 点分隔键名支持 (`group.subgroup.key`) -- [x] 优先级查询 (Temp → App → User → System) -- [x] 变更监听机制 (watch/unwatch) -- [x] 持久化存储 (JSON格式) -- [x] 同步/异步 sync -- [x] 类型安全查询 (模板 API) -- [x] 线程安全 - -#### 接口摘要 - -```cpp -namespace cf::config { - -enum class Layer { Temp, App, User, System }; - -class ConfigStore : public SimpleSingleton { -public: - // 查询操作 - template - std::optional query(const KeyView key); - template - Value query(const KeyView key, const Value& default_value); - template - std::optional query(const KeyView key, Layer layer); - bool has_key(const KeyView key); - - // 写入操作 - template - bool set(const KeyView key, const Value& v, Layer layer = Layer::App); - template - RegisterResult register_key(const Key& key, const Value& init_value, Layer layer = Layer::App); - UnRegisterResult unregister_key(const Key& key, Layer layer = Layer::App); - - // 监听操作 - WatcherHandle watch(const std::string& key_pattern, Watcher callback); - void unwatch(WatcherHandle handle); - - // 持久化 - void sync(SyncMethod m = SyncMethod::Async); - void reload(); -}; - -} -```bash - -#### 测试 - -- [x] `test/config_manager/config_store_test.cpp` - -#### 文档 - -- [x] `document/desktop/base/config_manager/` - API文档 -- [x] `document/HandBook/desktop/base/config_manager/` - 使用手册 - ---- - -### 3.6 Logger - 日志系统 (100%) - -**实现位置**: `desktop/base/logger/` - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/logger/include/cflog.h` | 便捷日志函数 | -| `desktop/base/logger/include/cflog/cflog_record.h` | 日志记录 | -| `desktop/base/logger/include/cflog/cflog_sink.h` | Sink 接口 | -| `desktop/base/logger/include/cflog/cflog_format.h` | 格式化接口 | -| `desktop/base/logger/include/cflog/cflog_format_factory.h` | 格式化工厂 | -| `desktop/base/logger/include/cflog/cflog_format_config.h` | 格式化配置 | -| `desktop/base/logger/include/cflog/cflog_format_flags.h` | 格式化标志 | -| `desktop/base/logger/src/logger/cflog.cpp` | 实现 | -| `desktop/base/logger/src/impl/cflog_impl.h` | 内部实现 | -| `desktop/base/logger/src/impl/cflog_impl.cpp` | 内部实现 | - -#### 功能实现 - -- [x] 多等级日志 (Trace/Debug/Info/Warning/Error) -- [x] ISink 接口 -- [x] 多种格式化器 -- [x] 异步日志 (后台线程处理) -- [x] 线程安全队列 -- [x] 等级过滤 -- [x] Tag 支持 -- [x] 源码位置自动捕获 (`std::source_location`) - -#### 接口摘要 - -```cpp -namespace cf::log { - -enum class level { Trace, Debug, Info, Warning, Error }; - -class Logger : public SimpleSingleton { -public: - bool log(level log_level, std::string_view msg, std::string_view tag, std::source_location loc); - void flush(); - void flush_sync(); - void setMininumLevel(const level lvl); - void add_sink(std::shared_ptr sink); - void remove_sink(ISink* sink); - void clear_sinks(); -}; - -// 便捷函数 -void trace(std::string_view msg, std::string_view tag = "CFLog", ...); -void debug(std::string_view msg, std::string_view tag = "CFLog", ...); -void info(std::string_view msg, std::string_view tag = "CFLog", ...); -void warning(std::string_view msg, std::string_view tag = "CFLog", ...); -void error(std::string_view msg, std::string_view tag = "CFLog", ...); - -class ISink { -public: - virtual bool write(const LogRecord& record) = 0; - virtual bool flush() = 0; - virtual bool setFormat(std::shared_ptr formatter); -}; - -} -```yaml - -#### 测试 - -- [x] `test/logger/logger_formatter_test.cpp` -- [x] `test/logger/logger_concurrency_test.cpp` - -#### 文档 - -- [x] `document/HandBook/desktop/base/logger/` - 使用手册 - ---- - -## 四、使用示例 - -### 主题切换 - -```cpp -#include - -auto& manager = cf::ui::core::ThemeManager::instance(); -manager.setThemeTo("theme.material.light"); -```text - -### 动画使用 - -```cpp -#include - -auto factory = std::make_unique(theme); -auto anim = factory->getAnimation("md.animation.fadeIn"); -if (anim) { - anim->start(); -} -```text - -### DPI 转换 - -```cpp -#include - -cf::ui::base::device::CanvasUnitHelper helper(2.0); -qreal pixels = helper.dpToPx(16.0); // 16dp -> 32px -```text - -### 配置读写 - -```cpp -#include - -using namespace cf::config; - -// 注册键 -Key theme_key{.full_key = "app.theme.name", .full_description = "Application theme"}; -ConfigStore::instance().register_key(theme_key, std::string("default"), Layer::App); - -// 查询配置 -KeyView kv{.group = "app.theme", .key = "name"}; -std::string theme = ConfigStore::instance().query(kv, "default"); - -// 修改配置 -ConfigStore::instance().set(kv, std::string("dark"), Layer::App); - -// 监听变化 -ConfigStore::instance().watch("app.theme.*", - [](const Key& k, const std::any* old, const std::any* new_val, Layer layer) { - // Handle change - }); - -// 同步到磁盘 -ConfigStore::instance().sync(SyncMethod::Async); -```text - -### 日志记录 - -```cpp -#include - -using namespace cf::log; - -// 便捷日志函数 -info("Application started", "MyApp"); -warning("Configuration file not found, using defaults", "Config"); -error("Failed to load resource", "Network"); - -// 设置日志等级 -set_level(level::Debug); - -// 使用 Logger 实例 -auto& logger = Logger::instance(); -logger.log(level::Info, "Message", "Tag", std::source_location::current()); - -// 刷新 -flush(); -```bash - ---- - -## 五、其他待完成模块 - -**注**: ConfigStore 和 Logger 已完成,以下是其他可选增强功能。 - -| 模块 | 优先级 | 依赖 | 状态 | -|------|--------|------|------| -| QSSProcessor | P2 | - | 待规划 | -| VariableResolver | P2 | - | 待规划 | -| HWTier 降级逻辑 | P1 | HWTier | ✅ 已实现 (hardware_tier_policy) | -| 屏幕参数检测 | P2 | - | 待规划 | -| 模拟器注入接口 | P2 | - | 待规划 | - -#### 主题加载器 (部分完成) - -**待完成**: -- JSON主题文件解析 -- 文件系统扫描 -- 主题继承链解析 -- 循环继承检测 -- QSS变量替换 - -**建议文件路径**: -- `ui/core/theme_loader.h` (新建) -- `ui/core/qss_processor.h` (新建) - -#### 变量解析器 (部分完成) - -**待完成**: -- 点分隔路径解析 (如 `text.primary`) -- 嵌套对象解析 -- 变量继承和回退 - -**建议文件路径**: -- `ui/core/variable_resolver.h` (新建) - ---- - -## 六、文件结构总览 - -### 已实现文件 - -#### ui/ 目录 (主题、动画、DPI) - -```text -ui/ -├── core/ -│ ├── theme_manager.h # 主题管理器 (核心) -│ ├── theme.h # ICFTheme接口 -│ ├── theme_factory.h # ThemeFactory抽象接口 -│ ├── color_scheme.h # ICFColorScheme颜色方案 -│ ├── motion_spec.h # IMotionSpec动画规范 -│ ├── font_type.h # IFontType字体接口 -│ └── radius_scale.h # IRadiusScale圆角接口 -├── components/ -│ ├── animation_factory_manager.h # 动画工厂管理器 -│ ├── animation.h # ICFAbstractAnimation基础接口 -│ ├── spring_animation.h # 弹簧动画 -│ ├── timing_animation.h # 时间曲线动画 -│ └── animation_group.h # 动画组 -└── base/ - ├── device_pixel.h # DPI转换工具 - ├── color.h # CFColor (HCT颜色) - ├── color_helper.h # 颜色工具函数 - ├── easing.h # 缓动曲线 - ├── geometry_helper.h # 几何工具 - └── math_helper.h # 数学工具 -```text - -#### desktop/base/config_manager/ (配置中心) ✅ - -```text -desktop/base/config_manager/ -├── include/ -│ ├── cfconfig.hpp # 主配置存储接口 -│ ├── cfconfig_key.h # 配置键定义 -│ ├── cfconfig_layer.h # 配置层级 -│ ├── cfconfig_notify_policy.h # 通知策略 -│ └── cfconfig/ -│ ├── cfconfig_result.h # 操作结果 -│ ├── cfconfig_watcher.h # 配置监听器 -│ └── cfconfig_path_provider.h # 路径提供者 -└── src/ - ├── cfconfig.cpp # 实现 - └── impl/ - ├── config_impl.h # 内部实现 - └── config_impl.cpp -```text - -#### desktop/base/logger/ (日志系统) ✅ - -```text -desktop/base/logger/ -├── include/ -│ ├── cflog.h # 便捷日志函数 -│ └── cflog/ -│ ├── cflog.hpp # 主 Logger 类 -│ ├── cflog_level.hpp # 日志等级 -│ ├── cflog_record.h # 日志记录 -│ ├── cflog_sink.h # Sink 接口 -│ ├── cflog_format.h # 格式化接口 -│ ├── cflog_format_factory.h # 格式化工厂 -│ ├── cflog_format_config.h # 格式化配置 -│ └── cflog_format_flags.h # 格式化标志 -└── src/ - ├── logger/ - │ └── cflog.cpp # 实现 - └── impl/ - ├── cflog_impl.h # 内部实现 - └── cflog_impl.cpp -```text - -### 待创建文件 (可选增强) - -```text -ui/core/ -├── theme_loader.h # 主题加载器 (待创建) -├── qss_processor.h # QSS处理器 (待创建) -└── variable_resolver.h # 变量解析器 (待创建) -```yaml - ---- - -## 七、下一步行动建议 - -**Phase 2 核心模块已全部完成!** 以下是可选增强功能。 - -### 优先级1 (高) - 可选增强 - -1. **完善主题加载器** - - 实现JSON主题文件解析 - - 实现主题继承 - - 预计工作量: 2-3天 - -2. **完善变量解析器** - - 实现点分隔路径解析 - - 实现变量回退机制 - - 预计工作量: 1-2天 - -### 优先级2 (中) - -3. **增强DPI管理** - - 添加模拟器注入接口 - - 支持热插拔屏幕 - - 预计工作量: 1-2天 - -4. **动画性能优化** - - 实现HWTier降级逻辑 - - 添加性能测试 - - 预计工作量: 2-3天 - -### 已完成模块 - -- [x] ConfigStore 配置中心 (100%) -- [x] Logger 日志系统 (100%) -- [x] ThemeEngine 主题引擎 (60%) -- [x] AnimationManager 动画管理器 (65%) -- [x] DPI management 分辨率适配 (80%) - ---- - -## 八、相关文档 - -- 原始TODO: ~~已归档~~(原 `02_base_library.md` 已删除) -- 设计文档: `../../design_stage/02_phase2_base_library.md` - ---- - -*文档版本: v2.0* -*生成时间: 2026-03-18* diff --git a/document/todo/done/03_input_layer_status.md b/document/todo/done/03_input_layer_status.md deleted file mode 100644 index a15ac02c7..000000000 --- a/document/todo/done/03_input_layer_status.md +++ /dev/null @@ -1,541 +0,0 @@ ---- -title: "Phase 3: 输入抽象层 - 状态文档" -description: "模块ID: Phase 3,状态: **模块ID**: Phase 3 -> **状态**: 未开始 -> **总体进度**: 0% -> **最后更新**: 2026-03-11 - ---- - -# 输入抽象层参考文档 - -> **文档版本**: 1.0 -> **最后更新**: 2026-03-05 -> **完成度**: 0% -> **说明**: 完全未实现,需要从头开发 - ---- - -## 一、模块概述 - -输入抽象层是CFDesktop项目的人机交互基础设施层,负责屏蔽底层输入设备差异,统一触摸、物理按键、旋钮等输入事件,并支持焦点导航模式。 - -### 核心职责 - -1. **统一输入分发** - 集中管理所有输入设备的注册、注销和事件分发 -2. **触摸处理** - 单击/双击检测、长按检测、多点触摸支持 -3. **按键处理** - 按键状态跟踪、长按检测、连击检测 -4. **旋钮处理** - AB相位解码、方向判定、速度计算、加速功能 -5. **手势识别** - 滑动、捏合、旋转手势识别 -6. **焦点导航** - 四方向导航、自定义焦点链、边界策略 - -### 设计目标 - -- **设备无关性**: 抽象底层硬件差异,提供统一的输入接口 -- **可扩展性**: 支持动态注册新类型输入设备 -- **模拟器友好**: 支持模拟器环境下的输入注入 -- **高性能**: 事件延迟 < 16ms,CPU占用 < 5% - ---- - -## 二、当前实现状态 - -### 总体完成度: 0% - -| 模块 | 完成度 | 实现位置 | 备注 | -|------|--------|----------|------| -| InputManager | 0% | - | 未实现 | -| TouchInputHandler | 0% | - | 未实现 | -| KeyInputHandler | 0% | - | 未实现 | -| RotaryInputHandler | 0% | - | 未实现 | -| GestureRecognizer | 0% | - | 未实现 | -| FocusNavigator | 0% | - | 未实现 | -| 原生设备驱动 | 0% | - | 未实现 | - ---- - -## 三、待完成项清单 - -### 3.1 InputManager - 统一分发层 (0%) - -**职责描述**: -- 设备注册/注销管理 -- 事件分发机制 -- 事件过滤器支持 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/InputManager.h` -- `include/CFDesktop/Base/Input/InputEvent.h` -- `include/CFDesktop/Base/Input/InputDevice.h` -- `src/base/input/InputManager.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -// 输入设备基类 -class InputDevice { -public: - virtual ~InputDevice() = default; - virtual QString deviceId() const = 0; - virtual QString deviceName() const = 0; - virtual DeviceType deviceType() const = 0; -}; - -// 输入事件基类 -class InputEvent { -public: - virtual ~InputEvent() = default; - virtual qint64 timestamp() const = 0; - virtual InputDevice* sourceDevice() const = 0; -}; - -// 输入管理器 -class InputManager : public QObject { - Q_OBJECT -public: - static InputManager& instance(); - - // 设备管理 - void registerDevice(std::unique_ptr device); - void unregisterDevice(const QString& deviceId); - QList devices() const; - InputDevice* device(const QString& deviceId) const; - - // 事件分发 - void dispatchEvent(std::unique_ptr event); - - // 事件过滤器 - void addEventFilter(QObject* filter); - void removeEventFilter(QObject* filter); - -signals: - void deviceRegistered(InputDevice* device); - void deviceUnregistered(const QString& deviceId); - void eventReceived(InputEvent* event); -}; - -} // namespace cf::base::input -```text - -### 3.2 TouchInputHandler - 触摸处理器 (0%) - -**职责描述**: -- 触摸点跟踪 (TouchPoint结构) -- 单击/双击检测 -- 长按检测 -- 多点触摸支持 -- 压力检测 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/TouchInputHandler.h` -- `src/base/input/TouchInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -struct TouchPoint { - int id; // 触摸点ID - QPointF pos; // 当前位置 - QPointF startPos; // 起始位置 - float pressure; // 压力值 - qint64 timestamp; // 时间戳 -}; - -class TouchInputHandler : public QObject { - Q_OBJECT -public: - explicit TouchInputHandler(QObject* parent = nullptr); - - // 配置参数 - void setClickThreshold(float pixels); // 点击移动阈值 - void setDoubleClickInterval(int ms); // 双击时间间隔 - void setLongPressTimeout(int ms); // 长按超时时间 - - // 触摸点管理 - void addTouchPoint(const TouchPoint& point); - void updateTouchPoint(int id, const QPointF& newPos); - void removeTouchPoint(int id); - -signals: - void clicked(const QPointF& pos); - void doubleClicked(const QPointF& pos); - void longPressed(const QPointF& pos); - void touchMoved(const QList& points); - -private: - QMap activePoints_; - float clickThreshold_ = 10.0f; - int doubleClickInterval_ = 400; - int longPressTimeout_ = 500; -}; - -} // namespace cf::base::input -```text - -### 3.3 KeyInputHandler - 按键处理器 (0%) - -**职责描述**: -- 按键状态跟踪 -- 长按检测 -- 连击检测 -- 按键配置支持 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/KeyInputHandler.h` -- `src/base/input/KeyInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -struct KeyEvent { - int keyCode; // 按键代码 - bool pressed; // true=按下, false=释放 - qint64 timestamp; // 时间戳 -}; - -class KeyInputHandler : public QObject { - Q_OBJECT -public: - explicit KeyInputHandler(QObject* parent = nullptr); - - // 按键处理 - void handleKeyEvent(const KeyEvent& event); - - // 配置参数 - void setLongPressThreshold(int ms); // 长按阈值 - void setMultiClickWindow(int ms); // 连击时间窗口 - -signals: - void keyPressed(int keyCode); - void keyReleased(int keyCode); - void keyLongPressed(int keyCode); // 长按触发 - void keyMultiClicked(int keyCode, int count); // 连击 - -private: - QMap keyPressTime_; - QMap keyClickCount_; - int longPressThreshold_ = 800; - int multiClickWindow_ = 300; -}; - -} // namespace cf::base::input -```text - -### 3.4 RotaryInputHandler - 旋钮处理器 (0%) - -**职责描述**: -- AB相位解码 -- 方向判定 -- 速度计算 -- 加速功能 - -**建议文件路径**: -- `include/CFDesktop/Base/Input/RotaryInputHandler.h` -- `src/base/input/RotaryInputHandler.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class RotaryDirection { Clockwise, CounterClockwise }; - -class RotaryInputHandler : public QObject { - Q_OBJECT -public: - explicit RotaryInputHandler(QObject* parent = nullptr); - - // AB相位输入 - void handlePhaseA(bool state); - void handlePhaseB(bool state); - - // 配置参数 - void setStepsPerRevolution(int steps); // 每圈步数 - void setAccelerationFactor(float factor); // 加速因子 - -signals: - void rotated(RotaryDirection direction, int steps); - void positionChanged(int absolutePosition); - -private: - bool phaseA_ = false; - bool phaseB_ = false; - int position_ = 0; - QList stepTimestamps_; // 用于速度计算 - float accelerationFactor_ = 1.0f; -}; - -} // namespace cf::base::input -```text - -### 3.5 GestureRecognizer - 手势识别器 (0%) - -**职责描述**: -- 滑动手势 (Swipe) -- 捏合手势 (Pinch) -- 旋转手势 (Rotation) - -**建议文件路径**: -- `include/CFDesktop/Base/Input/GestureRecognizer.h` -- `src/base/input/GestureRecognizer.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class SwipeDirection { Up, Down, Left, Right }; - -class GestureRecognizer : public QObject { - Q_OBJECT -public: - explicit GestureRecognizer(QObject* parent = nullptr); - - // 触摸输入 - void handleTouchPoints(const QList& points); - - // 配置参数 - void setSwipeThreshold(float pixels); // 滑动距离阈值 - void setSwipeTimeout(int ms); // 滑动超时 - void setPinchThreshold(float ratio); // 捏合阈值 - -signals: - void swipeDetected(SwipeDirection direction); - void pinchStarted(const QPointF& center); - void pinchZoomChanged(float scale); - void pinchFinished(); - void rotationStarted(const QPointF& center); - void rotationAngleChanged(float angle); - void rotationFinished(); - -private: - QList lastTouchPositions_; - qint64 lastTouchTime_ = 0; - float swipeThreshold_ = 50.0f; - int swipeTimeout_ = 300; -}; - -} // namespace cf::base::input -```text - -### 3.6 FocusNavigator - 焦点导航器 (0%) - -**职责描述**: -- 四方向导航算法 -- 自定义焦点链 -- 边界策略 (循环/停止) - -**建议文件路径**: -- `include/CFDesktop/Base/Input/FocusNavigator.h` -- `src/base/input/FocusNavigator.cpp` - -**关键接口设计**: -```cpp -namespace cf::base::input { - -enum class NavigationDirection { Up, Down, Left, Right }; -enum class BoundaryPolicy { Stop, Wrap, Jump }; - -class FocusNavigator : public QObject { - Q_OBJECT -public: - static FocusNavigator& instance(); - - // 焦点导航 - void navigate(NavigationDirection direction); - void setFocus(QWidget* widget); - QWidget* currentFocus() const; - - // 焦点策略 - void setBoundaryPolicy(BoundaryPolicy policy); - - // 自定义焦点链 - void addFocusChain(QWidget* from, NavigationDirection dir, QWidget* to); - void removeFocusChain(QWidget* from); - void clearFocusChains(); - -signals: - void focusChanged(QWidget* oldFocus, QWidget* newFocus); - void focusLost(QWidget* widget); - -private: - QWidget* currentFocus_ = nullptr; - BoundaryPolicy boundaryPolicy_ = BoundaryPolicy::Stop; - QMultiMap> focusChains_; -}; - -} // namespace cf::base::input -```bash - ---- - -## 四、原生设备驱动 - -### 4.1 EvdevDevice - Linux事件设备 (0%) - -**职责**: 读取 `/dev/input/eventX` 设备事件 - -**建议文件路径**: -- `src/base/input/native/EvdevDevice.cpp` -- `src/base/input/native/EvdevDevice.h` - -### 4.2 GPIOButton - GPIO按键 (0%) - -**职责**: 通过sysfs接口读取GPIO按键状态 - -**建议文件路径**: -- `src/base/input/native/GPIOButton.cpp` -- `src/base/input/native/GPIOButton.h` - -### 4.3 RotaryEncoder - 旋转编码器 (0%) - -**职责**: 读取GPIO编码器状态并解码 - -**建议文件路径**: -- `src/base/input/native/RotaryEncoder.cpp` -- `src/base/input/native/RotaryEncoder.h` - -### 4.4 SimulatedInput - 模拟器输入 (0%) - -**职责**: 为模拟器环境提供输入注入接口 - -**建议文件路径**: -- `src/base/input/simulator/SimulatedInput.cpp` -- `src/base/input/simulator/SimulatedInput.h` - ---- - -## 五、完成度百分比 - -| 模块 | 原计划需求 | 已实现 | 完成度 | -|------|-----------|--------|--------| -| InputManager | 100% | 0% | 0% | -| TouchInputHandler | 100% | 0% | 0% | -| KeyInputHandler | 100% | 0% | 0% | -| RotaryInputHandler | 100% | 0% | 0% | -| GestureRecognizer | 100% | 0% | 0% | -| FocusNavigator | 100% | 0% | 0% | -| 原生设备驱动 | 100% | 0% | 0% | -| **总体** | **100%** | **0%** | **0%** | - ---- - -## 六、关键文件路径 - -### 待创建文件 - -```text -include/CFDesktop/Base/Input/ -├── InputManager.h # 统一分发层 -├── InputEvent.h # 输入事件定义 -├── InputDevice.h # 输入设备基类 -├── TouchInputHandler.h # 触摸处理器 -├── KeyInputHandler.h # 按键处理器 -├── RotaryInputHandler.h # 旋钮处理器 -├── GestureRecognizer.h # 手势识别器 -└── FocusNavigator.h # 焦点导航器 - -src/base/input/ -├── InputManager.cpp -├── TouchInputHandler.cpp -├── KeyInputHandler.cpp -├── RotaryInputHandler.cpp -├── GestureRecognizer.cpp -├── FocusNavigator.cpp -├── native/ -│ ├── EvdevDevice.h/cpp # Linux evdev设备 -│ ├── GPIOButton.h/cpp # GPIO按键 -│ └── RotaryEncoder.h/cpp # 旋转编码器 -└── simulator/ - └── SimulatedInput.h/cpp # 模拟器输入注入 - -tests/unit/base/input/ -├── test_input_manager.cpp -├── test_touch_handler.cpp -├── test_key_handler.cpp -├── test_rotary_handler.cpp -├── test_gesture_recognizer.cpp -└── test_focus_navigator.cpp -```yaml - ---- - -## 七、下一步行动建议 - -### 优先级1 (高) - 核心框架 - -1. **实现InputManager核心框架** - - 优先级: 最高 (其他模块依赖) - - 预计工作量: 2-3天 - - 交付物: 设备注册、事件分发、过滤器支持 - -2. **实现TouchInputHandler** - - 优先级: 高 (触摸屏设备) - - 预计工作量: 3-4天 - - 交付物: 触摸点跟踪、点击/长按检测 - -### 优先级2 (中) - 特定设备 - -3. **实现KeyInputHandler** - - 优先级: 中 - - 预计工作量: 2-3天 - - 交付物: 按键状态跟踪、长按/连击检测 - -4. **实现FocusNavigator** - - 优先级: 中 (遥控器导航) - - 预计工作量: 2-3天 - - 交付物: 四方向导航、边界策略 - -### 优先级3 (低) - 增强功能 - -5. **实现RotaryInputHandler** - - 优先级: 低 (特定硬件) - - 预计工作量: 2天 - -6. **实现GestureRecognizer** - - 优先级: 低 (高级交互) - - 预计工作量: 3-4天 - -7. **原生设备驱动实现** - - 优先级: 低 (真机部署时) - - 预计工作量: 4-5天 - ---- - -## 八、验收标准 - -### 功能验收 - -- [ ] 触摸输入正常响应 (单击/双击/长按) -- [ ] 手势识别准确率 > 95% -- [ ] 焦点导航无死循环 -- [ ] 原生设备正常读取 (evdev/GPIO) - -### 性能验收 - -- [ ] 事件延迟 < 16ms -- [ ] CPU占用 < 5% -- [ ] 无内存泄漏 - -### 兼容性验收 - -- [ ] 模拟器和真机行为一致 -- [ ] Linux和Windows平台兼容 - ---- - -## 九、相关文档 - -- 原始TODO: [../base/02_input_layer.md](../base/02_input_layer.md) -- 设计文档: `../../design_stage/03_phase3_input_layer.md` -- 依赖模块: [Base库状态](./02_base_library_status.md) - ---- - -*最后更新: 2026-03-05* diff --git a/document/todo/done/04_simulator_status.md b/document/todo/done/04_simulator_status.md deleted file mode 100644 index 285664dbc..000000000 --- a/document/todo/done/04_simulator_status.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -title: "Phase 4: 多平台模拟器 - 状态文档" -description: "模块ID: Phase 4,状态: **模块ID**: Phase 4 -> **状态**: 未开始 -> **总体进度**: 0% -> **最后更新**: 2026-03-11 - ---- - -# 04 - Multi-Platform Simulator Reference - -## Module Overview - -The Multi-Platform Simulator module provides cross-platform device simulation capabilities for CFDesktop, enabling developers to test the application on various device configurations without physical hardware. This module is essential for embedded device development and cross-platform UI testing. - -**Module Purpose:** -- Simulate different hardware configurations (memory, CPU, display) -- Provide device frame visualization (phone, tablet, embedded terminal) -- Enable touch input visualization for touch-enabled interfaces -- Support hardware tier selection for performance testing -- DPI injection for testing different screen densities - ---- - -## Current Implementation Status - -**Completion: 0%** - -This module is currently **NOT IMPLEMENTED**. No files exist for the simulator functionality. - ---- - -## Required Components - -### 1. SimulatorWindow (Main Window) -**Status:** Not Implemented - -**Purpose:** Main simulator window hosting device frames and controls. - -**Required Features:** -- Device frame container -- Configuration panel (device type, resolution, DPI) -- Runtime controls (start, stop, reset) -- Screenshot capture -- Performance monitoring overlay - -**Key Files to Create:** -- `ui/simulator/simulator_window.h` -- `ui/simulator/simulator_window.cpp` - ---- - -### 2. DeviceFrame (Device Shell) -**Status:** Not Implemented - -**Purpose:** Visual frame representing physical device外观. - -**Required Features:** -- Predefined device frames (phone, tablet, embedded terminal) -- Custom frame support -- Screen area clipping -- Device-specific bezels and buttons -- Status bar simulation - -**Key Files to Create:** -- `ui/simulator/device/device_frame.h` -- `ui/simulator/device/device_frame.cpp` -- `ui/simulator/device/frame_presets.h` (preset definitions) - ---- - -### 3. TouchVisualizer -**Status:** Not Implemented - -**Purpose:** Visualize touch events for debugging touch interfaces. - -**Required Features:** -- Touch point rendering (circles at touch locations) -- Multi-touch support (up to 10 points) -- Touch path/trail visualization -- Tap duration indication -- Gesture recognition display - -**Key Files to Create:** -- `ui/simulator/visualizer/touch_visualizer.h` -- `ui/simulator/visualizer/touch_visualizer.cpp` - ---- - -### 4. HWTierSelector (Hardware Tier Selector) -**Status:** Not Implemented - -**Purpose:** Select hardware performance tiers for testing. - -**Required Features:** -- Tier presets (Low, Medium, High, Ultra) -- Custom hardware configuration -- Memory limit simulation -- CPU throttling simulation -- Storage speed simulation - -**Key Files to Create:** -- `ui/simulator/config/hw_tier_selector.h` -- `ui/simulator/config/hw_tier_selector.cpp` -- `ui/simulator/config/hw_profile.h` (profile definitions) - ---- - -### 5. DPI Injector -**Status:** Not Implemented - -**Purpose:** Inject custom DPI values for testing high/low density displays. - -**Required Features:** -- DPI value override -- Scaling factor simulation -- Font DPI injection -- Coordinate system transformation - -**Key Files to Create:** -- `ui/simulator/injector/dpi_injector.h` -- `ui/simulator/injector/dpi_injector.cpp` - ---- - -### 6. Hardware Mock -**Status:** Not Implemented - -**Purpose:** Mock hardware APIs for embedded simulation. - -**Required Features:** -- Memory query mock -- CPU info mock -- Storage mock -- Network condition simulation -- Sensor mock (accelerometer, gyroscope) - -**Key Files to Create:** -- `ui/simulator/mock/hardware_mock.h` -- `ui/simulator/mock/hardware_mock.cpp` -- `ui/simulator/mock/system_info_mock.h` - ---- - -## Implementation Priority - -### Phase 1 - Core Structure (P0) -1. **SimulatorWindow** - Basic window with container -2. **DeviceFrame** - Simple frame with screen area -3. **HWTierSelector** - Basic tier selection UI - -### Phase 2 - Visualization (P1) -4. **TouchVisualizer** - Touch event rendering -5. **DPI Injector** - DPI override capability - -### Phase 3 - Advanced Features (P2) -6. **HardwareMock** - Full hardware API mocking -7. **Performance Profiling** - FPS, memory monitoring -8. **Screenshot/Recording** - Capture capabilities - ---- - -## Key File Paths - -| Component | Header Path | Source Path | -|-----------|-------------|-------------| -| SimulatorWindow | `ui/simulator/simulator_window.h` | `ui/simulator/simulator_window.cpp` | -| DeviceFrame | `ui/simulator/device/device_frame.h` | `ui/simulator/device/device_frame.cpp` | -| TouchVisualizer | `ui/simulator/visualizer/touch_visualizer.h` | `ui/simulator/visualizer/touch_visualizer.cpp` | -| HWTierSelector | `ui/simulator/config/hw_tier_selector.h` | `ui/simulator/config/hw_tier_selector.cpp` | -| DPI Injector | `ui/simulator/injector/dpi_injector.h` | `ui/simulator/injector/dpi_injector.cpp` | -| HardwareMock | `ui/simulator/mock/hardware_mock.h` | `ui/simulator/mock/hardware_mock.cpp` | - ---- - -## Design Considerations - -### Qt Integration -- Use `QWidget` as base for all simulator components -- Leverage `QGraphicsView` for device frame rendering -- Use `QTouchEvent` for touch simulation - -### Platform Support -- Windows: Full support -- Linux: Full support -- macOS: Full support (with limitations on touch) - -### Performance -- Simulator should not impact application performance significantly -- Mock operations should be lightweight -- Visualization should use hardware acceleration when available - ---- - -## Next Steps - -1. **Create directory structure:** - ``` - ui/simulator/ - ├── simulator_window.h/cpp - ├── device/ - ├── visualizer/ - ├── config/ - ├── injector/ - └── mock/ - ``` - -2. **Implement SimulatorWindow** - Start with basic container widget - -3. **Add CMakeLists.txt entries** for new simulator module - -4. **Integrate with existing Application class** - Add simulator launcher - -5. **Write unit tests** for each component - ---- - -## Related Documents - -- `01_architecture_ref.md` - Overall architecture -- `05_testing_ref.md` - Testing infrastructure -- `10_ui_embedding_ref.md` - UI embedding for embedded systems - ---- - -*Last Updated: 2026-03-05* -*Status: Not Implemented* diff --git a/document/todo/done/05_testing_status.md b/document/todo/done/05_testing_status.md deleted file mode 100644 index 43138b1b1..000000000 --- a/document/todo/done/05_testing_status.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: "Phase 5: 测试系统 - 状态文档" -description: "模块ID: Phase 5,状态: 🚧 基础框架完成,UI控件测试缺失" ---- - -# Phase 5: 测试系统 - 状态文档 - -> **模块ID**: Phase 5 -> **状态**: 🚧 基础框架完成,UI控件测试缺失 -> **总体进度**: ~55% -> **最后更新**: 2026-03-27 - ---- - -## 一、模块概述 - -测试系统负责为 CFDesktop 项目提供完整的单元测试、集成测试和性能测试框架。 - -**模块职责:** -- 提供核心工具库的单元测试框架 -- 支持 UI 组件测试 -- 支持系统信息查询测试 -- 支持启动序列测试 - ---- - -## 二、完成状态总览 - -| 模块 | 测试文件数 | 覆盖率 | 状态 | -|------|-----------:|-------:|------| -| Base Utilities | 6 | 90% | ✅ Good | -| UI Base | 6 | 60% | ⚠️ Fair | -| UI Components | 5 | 60% | ⚠️ Fair | -| UI Core | 1 | 30% | ❌ Poor | -| UI Widgets | 0 | **0%** | ❌ **Critical** | -| System Queries | 2 | 70% | ⚠️ Fair | -| Logger | 4 | 85% | ✅ Good | -| ConfigManager | 1 | 80% | ✅ Good | -| Desktop | 1 | 5% | ❌ Poor | -| **Overall** | **26** | **55%** | ⚠️ **Fair** | - ---- - -## 三、已完成工作 - -### 3.1 测试基础框架 (100%) - -#### Google Test 配置 -- Google Test v1.14.0 集成 -- CMake `setup_third_party` 自动下载 -- Windows 特殊配置 (gtest_force_shared_crt ON) -- CTest 集成 -- 自定义 `add_gtest_executable` 辅助函数 - -#### 测试运行脚本 -- `run_all_tests.ps1` (Windows PowerShell) -- `run_all_tests.sh` (Linux/macOS Bash) - ---- - -### 3.2 测试目录结构 - -```text -test/ -├── CMakeLists.txt # 测试构建配置 -├── base/ # 基础工具库测试 -│ ├── weak_ptr/ -│ │ └── weak_ptr_test.cpp # WeakPtr 实现测试 -│ ├── expected/ -│ │ └── expected_test.cpp # Expected/Result 类型测试 -│ ├── hash/ -│ │ └── constexpr_fnv1a_test.cpp # 编译时哈希测试 -│ └── scope_guard/ -│ └── scope_guard_test.cpp # RAII 作用域守卫测试 -├── ui/ # UI 层测试 -│ ├── base/ -│ │ ├── device_pixel_test.cpp # 设备像素转换测试 -│ │ ├── easing_test.cpp # 缓动曲线测试 -│ │ ├── math_helper_test.cpp # 数学助手测试 (全面) -│ │ ├── color_test.cpp # 颜色工具测试 -│ │ ├── color_helper_test.cpp # 颜色助手测试 -│ │ └── geometry_helper_test.cpp # 几何助手测试 -│ ├── components/ -│ │ ├── elevation_controller_test.cpp # 高度阴影测试 -│ │ ├── focus_ring_test.cpp # 焦点指示器测试 -│ │ ├── painter_layer_test.cpp # 绘制层测试 -│ │ ├── ripple_helper_test.cpp # 涟漪效果测试 -│ │ └── state_machine_test.cpp # 状态机测试 -│ └── core/ -│ └── token_test.cpp # 主题 Token 测试 -├── system/ # 系统查询测试 -│ ├── test_memory_info_query.cpp # 内存信息查询测试 -│ └── test_cpu_info_query.cpp # CPU 信息查询测试 -└── boot_test/ # 启动序列测试 - ├── boot_detect.cpp # 启动检测 - ├── boot_test_core.cpp # 核心启动测试 - ├── boot_test_gui.cpp # GUI 启动测试 - ├── CMakeLists.txt - └── README.md # 启动测试文档 -```bash - ---- - -### 3.3 已实现测试清单 - -#### Base Utilities 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **expected_test.cpp** | `test/base/expected/expected_test.cpp` | `cf::expected` Result 类型:构造/销毁、值和错误访问、赋值、单子操作 (and_then, or_else, transform)、比较、交换、异常处理、移动语义、const 正确性 | -| **scope_guard_test.cpp** | `test/base/scope_guard/scope_guard_test.cpp` | RAII 作用域守卫基本功能 | -| **constexpr_fnv1a_test.cpp** | `test/base/hash/constexpr_fnv1a_test.cpp` | FNV1a 编译时哈希、抗冲突性 | -| **weak_ptr_test.cpp** | `test/base/weak_ptr/weak_ptr_test.cpp` | `cf::WeakPtr` 所有权语义、生命周期管理 | - -#### UI Base 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **math_helper_test.cpp** | `test/ui/base/math_helper_test.cpp` | `lerp` (7), `clamp` (6), `remap` (8), `cubicBezier` (7), `springStep` (7), `lerpAngle` (10) 测试用例 | -| **device_pixel_test.cpp** | `test/ui/base/device_pixel_test.cpp` | 设备像素比转换、DP 到像素转换 | -| **easing_test.cpp** | `test/ui/base/easing_test.cpp` | QEasingCurve 包装器、Material 缓动曲线验证 | -| **color_test.cpp** | `test/ui/base/color_test.cpp` | 颜色空间转换、Material 颜色系统、对比度计算 | -| **color_helper_test.cpp** | `test/ui/base/color_helper_test.cpp` | 颜色辅助工具 | -| **geometry_helper_test.cpp** | `test/ui/base/geometry_helper_test.cpp` | 圆角矩形路径生成、矩形裁剪 | - -#### UI Components 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **state_machine_test.cpp** | `test/ui/components/state_machine_test.cpp` | Material 行为层:状态转换 (8)、组合状态 (2)、透明度计算 (6)、状态优先级 (2) | -| **ripple_helper_test.cpp** | `test/ui/components/ripple_helper_test.cpp` | 涟漪创建和生命周期、多涟漪支持、淡入动画 | -| **elevation_controller_test.cpp** | `test/ui/components/elevation_controller_test.cpp` | 阴影渲染、高度级别验证 (0-5)、暗主题色调覆盖 | -| **focus_ring_test.cpp** | `test/ui/components/focus_ring_test.cpp` | 焦点环渲染、Material 规范合规 (3dp 宽度, 3dp 内边距) | -| **painter_layer_test.cpp** | `test/ui/components/painter_layer_test.cpp` | 颜色覆盖层、Alpha 混合验证 | - -#### UI Core 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **token_test.cpp** | `test/ui/core/token_test.cpp` | Token 系统验证、颜色方案 Token、排版 Token | - -#### System 测试 - -| 测试文件 | 路径 | 覆盖内容 | -|---------|------|---------| -| **test_memory_info_query.cpp** | `test/system/test_memory_info_query.cpp` | 内存信息查询、跨平台内存 API 验证 | -| **test_cpu_info_query.cpp** | `test/system/test_cpu_info_query.cpp` | CPU 信息查询、处理器计数检测 | - ---- - -## 四、待完成模块 - -### 4.1 Widget 测试 (P0 - Critical) - -**状态**: 完全未实现 (0%) - -**关键缺口**: 19个P0/P1控件全部无单元测试 - -**P0 核心控件** (7个): -- `button_test.cpp` - Button 控件测试 -- `textfield_test.cpp` - TextField 控件测试 -- `textarea_test.cpp` - TextArea 控件测试 -- `checkbox_test.cpp` - CheckBox 控件测试 -- `radiobutton_test.cpp` - RadioButton 控件测试 -- `label_test.cpp` - Label 控件测试 -- `groupbox_test.cpp` - GroupBox 控件测试 - -**P1 常用控件** (12个): -- `slider_test.cpp` - Slider 控件测试 -- `progressbar_test.cpp` - ProgressBar 控件测试 -- `switch_test.cpp` - Switch 控件测试 -- `combobox_test.cpp` - ComboBox 控件测试 -- `spinbox_test.cpp` - SpinBox 控件测试 -- `doublespinbox_test.cpp` - DoubleSpinBox 控件测试 -- `listview_test.cpp` - ListView 控件测试 -- `treeview_test.cpp` - TreeView 控件测试 -- `tableview_test.cpp` - TableView 控件测试 -- `tabview_test.cpp` - TabView 控件测试 -- `scrollview_test.cpp` - ScrollView 控件测试 -- `separator_test.cpp` - Separator 控件测试 - -**目标路径**: `test/ui/widgets/` - -### 4.2 Animation 测试 (P1) - -**状态**: 未实现 - -必需的测试文件: -- `timing_animation_test.cpp` - 时间动画测试 -- `spring_animation_test.cpp` - 弹簧动画测试 -- `animation_group_test.cpp` - 动画组测试 -- `animation_factory_test.cpp` - 动画工厂测试 - -**目标路径**: `test/ui/animation/` - -### 4.3 Theme 测试 (P1) - -**状态**: 部分 (token_test.cpp 已存在) - -缺失的测试: -- `theme_manager_test.cpp` - 主题管理器测试 -- `color_scheme_test.cpp` - 颜色方案测试 -- `motion_spec_test.cpp` - 动画规格测试 - -**目标路径**: `test/ui/theme/` - -### 4.4 集成/UI自动化/性能测试 (P2) - -**状态**: 未实现 - -**集成测试**: -- `base_integration_test.cpp` - Base 库集成测试 -- `sdk_integration_test.cpp` - SDK 集成测试 -- `shell_integration_test.cpp` - Shell 集成测试 - -**UI 自动化测试**: -- 鼠标事件模拟 -- 键盘事件模拟 -- 触摸事件模拟 -- 截图对比测试 - -**性能基准测试**: -- `widget_benchmark.cpp` - 控件渲染性能 -- `animation_benchmark.cpp` - 动画帧率测试 -- `memory_benchmark.cpp` - 内存使用分析 - ---- - -## 五、测试金字塔 - -```text - /\ - / \ - / E2E\ (端到端测试 - 少量) - /------\ - / 集成 \ (集成测试 - 适量) - /--------\ - / UI 自动化 \ (UI 自动化 - 中等) - /------------\ - / 单元测试 \ (单元测试 - 大量,已完成 ~60%) - /________________\ -```yaml - ---- - -## 六、运行测试 - -### 构建测试 - -```bash -# 从项目根目录 -cmake -B out/build_test -DBUILD_TESTING=ON -cmake --build out/build_test -```text - -### 运行所有测试 - -```bash -./out/build_test/bin/cf_desktop_tests -```text - -### 运行特定测试 - -```bash -# 运行特定测试套件 -./out/build_test/bin/cf_desktop_tests --gtest_filter=MathHelperTest.* - -# 运行特定测试用例 -./out/build_test/bin/cf_desktop_tests --gtest_filter=MathHelperTest.Lerp* -```text - -### 使用脚本运行 - -**Windows**: -```powershell -.\scripts\run_all_tests.ps1 -```text - -**Linux/macOS**: -```bash -./scripts/run_all_tests.sh -```yaml - ---- - -## 七、下一步行动建议 - -1. **[P0] 添加 Widget 测试** - 为所有 P0 Material 控件创建单元测试 -2. **[P1] 添加 Animation 测试** - 测试时间/弹簧动画和动画组 -3. **[P1] 完善 Theme 测试** - 补充主题管理器和颜色方案测试 -4. **[P2] 创建 Mock 框架** - 用于硬件抽象层测试 -5. **[P2] 添加 UI 自动化** - 鼠标/键盘/触摸模拟测试 -6. **[P2] 性能基准测试** - 建立基线性能指标 -7. **[P2] CI 集成** - 将自动化测试添加到 CI/CD 流程 - ---- - -## 八、相关文档 - ---- - -*文档版本: v1.0* -*生成时间: 2026-03-11* diff --git a/document/todo/done/06_infrastructure_status.md b/document/todo/done/06_infrastructure_status.md deleted file mode 100644 index 6388e93b4..000000000 --- a/document/todo/done/06_infrastructure_status.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -title: "Phase A: 基础设施部分完成状态" -description: "状态: ✅ 部分完成,总体进度: ~50%" ---- - -# Phase A: 基础设施部分完成状态 - -> **状态**: ✅ 部分完成 -> **总体进度**: ~50% -> **最后更新**: 2026-03-21 - ---- - -## 一、已完成模块 - -### 1.1 GPU 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/gpu/gpu.h` | GPU/显示信息接口 | -| `base/system/gpu/gpu.cpp` | 跨平台实现 | -| `base/system/gpu/private/linux_impl/gpu_info.h` | Linux 接口 | -| `base/system/gpu/private/linux_impl/gpu_info.cpp` | Linux 实现 | -| `base/system/gpu/private/win_impl/gpu_info.h` | Windows 接口 | -| `base/system/gpu/private/win_impl/gpu_info.cpp` | Windows 实现 | - -#### 功能实现 - -- [x] GPU 信息检测 (名称、厂商、驱动版本) -- [x] 显示器信息检测 (分辨率、刷新率、DPI) -- [x] 环境评分算法 (GPU 50分 + 显示 50分) -- [x] 档位判定 (Low < 45, Mid 45-74, High ≥ 75) -- [x] Linux DRM 设备检测 -- [x] Linux DeviceTree SoC 检测 -- [x] WSL2 GPU 探测 (/dev/dxg) -- [x] Windows DXGI 检测 - -#### 示例 - -- 示例文件: `example/base/system/example_gpu_info.cpp` - ---- - -### 1.2 Network 检测器 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `base/include/system/network/network.h` | 网络信息接口 | -| `base/system/network/network.cpp` | 基于 Qt 的跨平台实现 | - -#### 功能实现 - -- [x] IpAddress 结构体 (IPv4/IPv6) -- [x] InterfaceInfo 结构体 (网卡信息) -- [x] AddressEntry 结构体 (IP地址条目) -- [x] InterfaceFlags 结构体 (接口标志) -- [x] NetworkStatus 结构体 (网络状态) -- [x] Reachability 枚举 (网络可达性) -- [x] TransportMedium 枚举 (传输介质) -- [x] DnsEligibility 枚举 (DNS可达性) -- [x] getNetworkInfo() 函数 -- [x] interfaceTypeName() 函数 - -#### 示例 - -- 示例文件: `example/base/system/example_network_info.cpp` - ---- - -### 1.3 ConfigStore 配置中心 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/config_manager/src/cfconfig.cpp` | 主要实现 | -| `desktop/base/config_manager/include/cfconfig_layer.h` | 层级定义 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_watcher.h` | 监听器定义 | -| `desktop/base/config_manager/include/cfconfig/cfconfig_path_provider.h` | 路径提供者 | - -#### 功能实现 - -- [x] 四层存储模型 (Temp > App > User > System) -- [x] 配置变更监听 (ConfigWatcher) -- [x] 通配符模式匹配 -- [x] 两种通知策略 (Immediate/Manual) -- [x] INI 格式持久化 (使用 QSettings) -- [x] 线程安全 (使用 shared_mutex) -- [x] 类型安全查询 (int, double, bool, string, QVariant) - -**注意**: 使用 INI 格式而非设计文档中的 JSON 格式 - -#### 配置文件路径 - -- System: `{app_dir}/system.ini`(CFDesktop 自管理目录内) -- User: `~/.config/cfdesktop/user.ini` -- App: `config/app.ini` - ---- - -### 1.4 Logger 日志系统 (100%) - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/base/logger/include/cflog/cflog_sink.h` | Sink 接口 | -| `desktop/base/logger/include/cflog/sinks/console_sink.h` | 控制台输出 | -| `desktop/base/logger/include/cflog/sinks/file_sink.h` | 文件输出 | -| `desktop/base/logger/include/cflog.h` | CF_LOG 宏系列 | - -#### 功能实现 - -- [x] 单例模式 Logger -- [x] 异步日志 (无锁 MPSC 队列) -- [x] ConsoleSink (stdout 输出) -- [x] FileSink (文件输出,支持追加/截断) -- [x] 多种日志级别 (TRACE, DEBUG, INFO, WARNING, ERROR) -- [x] CF_LOG 宏系列 (使用 C++ 模板) -- [x] 格式化日志 (tracef, debugf, infof 等) -- [x] 标签支持 -- [x] 自动捕获源码位置 - -#### 示例 - -- 示例文件: `example/desktop/base/logger/logger_init.cpp` - ---- - -## 二、待完成模块 - -### 2.1 HWTier 核心框架 (0%) - -- [ ] `HWTier` 枚举定义 (Low/Mid/High) -- [ ] `HardwareProbe` 主类 -- [ ] `HardwareInfo` 结构体 -- [ ] `CapabilityPolicy` 策略引擎 -- [ ] 档位覆写机制 - -### 2.2 CrashHandler 崩溃处理 (0%) - -- [ ] CrashHandler 类 -- [ ] CrashReport 结构 -- [ ] CrashReporter 弹窗程序 -- [ ] Watchdog 守护进程 -- [ ] 信号捕获 (SIGSEGV, SIGABRT 等) -- [ ] 堆栈回溯 - -### 2.3 IPC 进程间通信 (0%) - -- [ ] IPCMessage 消息格式 -- [ ] IPCClient 客户端 -- [ ] IPCServer 服务器 -- [ ] ServiceLocator 服务定位器 - ---- - -## 三、相关文档 - -- 原始 TODO: [`../desktop/06_infrastructure.md`](../desktop/06_infrastructure.md) -- GPU/Network 状态: [`./01_hardware_probe_status.md`](./01_hardware_probe_status.md) -- Logger 文档: (见 desktop/base/logger/) - ---- - -*最后更新: 2026-03-21* diff --git a/document/todo/done/13_widget_apps_status.md b/document/todo/done/13_widget_apps_status.md deleted file mode 100644 index ab974e48e..000000000 --- a/document/todo/done/13_widget_apps_status.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: "Phase G: Widget 应用与示例程序状态" -description: "完成日期: 2026-03-21,总体进度: 100%" ---- - -# Phase G: Widget 应用与示例程序状态 - -> **状态**: ✅ 已完成 -> **完成日期**: 2026-03-21 -> **总体进度**: 100% - ---- - -## 一、应用框架 - -### 1.1 Application 基础框架 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/widget/application_support/application.h` | 基础应用框架 | -| `ui/widget/application_support/application.cpp` | 实现文件 | - -#### 功能实现 - -- [x] 继承自 QApplication -- [x] 主题管理集成 -- [x] 动画管理器集成 -- [x] Token-based API 支持 -- [x] 全局访问接口 - ---- - -### 1.2 MaterialApplication 框架 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `ui/widget/material/application/material_application.h` | Material 应用框架 | -| `ui/widget/material/application/material_application.cpp` | 实现文件 | - -#### 功能实现 - -- [x] 预配置 Material Design 3 主题 -- [x] 自动注册亮色和暗色主题 -- [x] 完整的 Material Design 组件支持 - ---- - -## 二、Material Widget Gallery - -### 2.1 主程序 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `example/ui/widget/material_example/main.cpp` | 主入口 | -| `example/ui/widget/material_example/MainWindow.h` | 主窗口 | -| `example/ui/widget/material_example/MainWindow.cpp` | 主窗口实现 | - -#### 功能特性 - -- [x] 画廊形式展示 (左侧导航,右侧展示) -- [x] 实时预览和交互 -- [x] 每个控件独立演示页面 -- [x] 统一的 Material Design 风格 - ---- - -### 2.2 已实现的 Demo 程序 (19个) - -| # | Demo 名称 | 头文件 | 实现文件 | -|---|----------|--------|----------| -| 1 | ButtonDemo | `demos/ButtonDemo.h` | `demos/ButtonDemo.cpp` | -| 2 | CheckBoxDemo | `demos/CheckBoxDemo.h` | `demos/CheckBoxDemo.cpp` | -| 3 | ComboBoxDemo | `demos/ComboBoxDemo.h` | `demos/ComboBoxDemo.cpp` | -| 4 | DoubleSpinBoxDemo | `demos/DoubleSpinBoxDemo.h` | `demos/DoubleSpinBoxDemo.cpp` | -| 5 | GroupBoxDemo | `demos/GroupBoxDemo.h` | `demos/GroupBoxDemo.cpp` | -| 6 | LabelDemo | `demos/LabelDemo.h` | `demos/LabelDemo.cpp` | -| 7 | ListViewDemo | `demos/ListViewDemo.h` | `demos/ListViewDemo.cpp` | -| 8 | ProgressBarDemo | `demos/ProgressBarDemo.h` | `demos/ProgressBarDemo.cpp` | -| 9 | RadioButtonDemo | `demos/RadioButtonDemo.h` | `demos/RadioButtonDemo.cpp` | -| 10 | ScrollViewDemo | `demos/ScrollViewDemo.h` | `demos/ScrollViewDemo.cpp` | -| 11 | SeparatorDemo | `demos/SeparatorDemo.h` | `demos/SeparatorDemo.cpp` | -| 12 | SliderDemo | `demos/SliderDemo.h` | `demos/SliderDemo.cpp` | -| 13 | SpinBoxDemo | `demos/SpinBoxDemo.h` | `demos/SpinBoxDemo.cpp` | -| 14 | SwitchDemo | `demos/SwitchDemo.h` | `demos/SwitchDemo.cpp` | -| 15 | TabViewDemo | `demos/TabViewDemo.h` | `demos/TabViewDemo.cpp` | -| 16 | TableViewDemo | `demos/TableViewDemo.h` | `demos/TableViewDemo.cpp` | -| 17 | TextAreaDemo | `demos/TextAreaDemo.h` | `demos/TextAreaDemo.cpp` | -| 18 | TextFieldDemo | `demos/TextFieldDemo.h` | `demos/TextFieldDemo.cpp` | -| 19 | TreeViewDemo | `demos/TreeViewDemo.h` | `demos/TreeViewDemo.cpp` | - ---- - -## 三、已实现的 Material Design 组件 - -### P0 核心控件 (7/7) ✅ - -1. **Button** - 按钮 (5种变体: Filled, Tonal, Outlined, Text, Elevated) -2. **TextField** - 单行文本输入 -3. **TextArea** - 多行文本编辑器 -4. **Label** - 显示文本或图像 -5. **CheckBox** - 二态选择控件 -6. **RadioButton** - 互斥选择控件 -7. **GroupBox** - 容器/分组框 - -### P1 控件 (12/12) ✅ - -1. **ComboBox** - 下拉列表 -2. **Slider** - 滑块 -3. **ProgressBar** - 进度条 -4. **Switch** - 开关 -5. **ListView** - 列表视图 -6. **TreeView** - 树视图 -7. **TableView** - 表格视图 -8. **TabView** - 标签页 -9. **ScrollView** - 滚动区域 -10. **Separator** - 分隔线 -11. **SpinBox** - 整数微调框 -12. **DoubleSpinBox** - 浮点微调框 - ---- - -## 四、其他示例程序 - -### GUI 相关示例 - -- Material Color Scheme Gallery -- Material Typography Gallery -- Material Motion Spec Gallery -- Material Radius Scale Gallery -- Theme Gallery -- Toast 演示 - -### 系统示例 - -- CPU 信息示例 -- 内存信息示例 -- GPU 信息示例 -- 网络信息示例 - ---- - -## 五、相关文档 - -- 原始 TODO: [`../desktop/13_widget_apps.md`](../desktop/13_widget_apps.md) -- UI 框架状态: [99_ui_material_framework_status](./99_ui_material_framework_status.md) -- P1 控件状态: [13_widget_apps](../desktop/13_widget_apps.md) - ---- - -*最后更新: 2026-03-21* diff --git a/document/todo/done/14_display_backend_status.md b/document/todo/done/14_display_backend_status.md deleted file mode 100644 index c3251f27e..000000000 --- a/document/todo/done/14_display_backend_status.md +++ /dev/null @@ -1,353 +0,0 @@ ---- -title: "Phase H: 显示后端与窗口管理系统状态" -description: "状态: ✅ 核心完成,完成日期: 2026-03-30" ---- - -# Phase H: 显示后端与窗口管理系统状态 - -> **状态**: ✅ 核心完成 -> **完成日期**: 2026-03-30 -> **总体进度**: ~70% (两个平台后端已完成,Wayland/嵌入式待实现) - ---- - -## 一、显示服务器抽象架构 - -### 1.1 IDisplayServerBackend 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IDisplayServerBackend.h` | 显示服务器后端抽象接口 | -| `desktop/ui/components/IDisplayServerBackend.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 三种运行模式定义 (`DisplayServerRole`) - - Client: 作为应用运行在已有桌面环境中 - - Compositor: CFDesktop 自己就是显示服务器 - - DirectRender: 直接渲染到帧缓冲 (EGLFS/linuxfb) -- [x] 能力描述 (`DisplayServerCapabilities`) - - `canManageExternalWindows` — 外部窗口管理能力 - - `needsOwnCompositor` — 是否需要独立合成器 - - `supportsWaylandProtocol` / `supportsX11Protocol` — 协议支持 -- [x] 生命周期方法: `initialize()` / `shutdown()` / `runEventLoop()` -- [x] 窗口后端访问: `windowBackend()` -- [x] 多输出支持: `outputs()` (多显示器 QRect 列表) -- [x] 信号: `outputChanged()`, `externalWindowAppeared()`, `externalWindowDisappeared()` - ---- - -### 1.2 IWindowBackend 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IWindowBackend.h` | 窗口后端抽象接口 | -| `desktop/ui/components/IWindowBackend.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 窗口创建/销毁: `createWindow()` / `destroyWindow()` -- [x] 窗口列表查询: `windows()` -- [x] 能力查询: `capabilities()` -- [x] 信号: `window_came` / `window_gone` - ---- - -### 1.3 IWindow 接口 - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/IWindow.h` | 平台无关窗口接口 | -| `desktop/ui/components/IWindow.cpp` | 接口实现 | - -#### 功能实现 - -- [x] 窗口标识: `windowID()` (win_id_t = QString) -- [x] 窗口属性: `title()`, `geometry()` -- [x] 窗口操作: `set_geometry()`, `requestClose()`, `raise()` -- [x] 信号: `closeRequested()`, `titleChanged()`, `geometryChanged()` - ---- - -## 二、Windows 平台后端 - -### 2.1 WindowsDisplayServerBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_display_server_backend.h` | Windows 显示服务器后端 | -| `desktop/ui/platform/windows/windows_display_server_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] 角色: Compositor (伪桌面模式) -- [x] Win32 事件钩子 (`SetWinEventHook`) 追踪第三方窗口 -- [x] `EnumWindows` 扫描已有窗口 -- [x] 能力: - - `canManageExternalWindows = true` - - `needsOwnCompositor = false` (Windows DWM 处理) - - 硬件加速、透明度、多窗口、VSync、截图 - -### 2.2 WindowsWindowBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_window_backend.h` | Windows 窗口后端 | -| `desktop/ui/platform/windows/windows_window_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] `SetWinEventHook` + `WINEVENT_OUTOFCONTEXT` 事件追踪 -- [x] `EnumWindows` 初始扫描 -- [x] 事件处理: `EVENT_OBJECT_CREATE` / `SHOW` / `DESTROY` -- [x] 智能窗口过滤 (`shouldTrackWindow()`): - - 必须可见、顶层、有标题 - - 跳过自身进程窗口 - - 过滤工具窗口、桌面、任务栏、IME 窗口 - -### 2.3 WindowsWindow - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/windows/windows_window.h` | Win32 HWND 窗口包装 | -| `desktop/ui/platform/windows/windows_window.cpp` | 实现 | - -#### 功能实现 - -- [x] 适配 Win32 HWND → IWindow 接口 -- [x] 标题: `GetWindowTextLengthW` / `GetWindowTextW` -- [x] 几何: `GetWindowRect` / `SetWindowPos` -- [x] 关闭: `PostMessageW(WM_CLOSE)` -- [x] 置顶: `SetForegroundWindow` / `BringWindowToTop` - ---- - -## 三、Linux/WSL X11 平台后端 - -### 3.1 WSLX11DisplayServerBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.h` | X11 显示服务器后端 | -| `desktop/ui/platform/linux_wsl/wsl_x11_display_server_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] 角色: Compositor (通过 X11 追踪实现伪合成器) -- [x] XCB 连接管理 (`xcb_connect`) -- [x] 根窗口监控 -- [x] 条件编译: `CFDESKTOP_HAS_XCB` - -### 3.2 WSLX11WindowBackend - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.h` | X11 窗口后端 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window_backend.cpp` | 实现 | - -#### 功能实现 - -- [x] XCB 协议通信 -- [x] `SubstructureNotifyMask` 根窗口事件监控 -- [x] `QSocketNotifier` 集成 XCB 事件到 Qt 事件循环 -- [x] 事件处理: - - `XCB_CREATE_NOTIFY` → 窗口创建 - - `XCB_DESTROY_NOTIFY` → 窗口销毁 - - `XCB_MAP_NOTIFY` → 窗口映射 - - `XCB_CONFIGURE_NOTIFY` → 几何变化 - - `XCB_PROPERTY_NOTIFY` → 属性变化 (标题等) -- [x] Atom 缓存 (`_NET_WM_NAME`, `WM_NAME`, `_NET_WM_PID`, `WM_PROTOCOLS`, `WM_DELETE_WINDOW`, `_NET_WM_WINDOW_TYPE`) -- [x] 窗口过滤: 可见性、标题、PID、窗口类型 (排除 dock/desktop) - -### 3.3 WSLX11Window - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/platform/linux_wsl/wsl_x11_window.h` | X11 窗口包装 | -| `desktop/ui/platform/linux_wsl/wsl_x11_window.cpp` | 实现 | - -#### 功能实现 - -- [x] 适配 xcb_window_t → IWindow 接口 -- [x] 标题: `_NET_WM_NAME` (UTF-8) → `WM_NAME` 降级 -- [x] 几何: `xcb_get_geometry` + `xcb_translate_coordinates` -- [x] 设置几何: `xcb_configure_window` -- [x] 优雅关闭: `xcb_client_message_event` + `WM_DELETE_WINDOW` -- [x] 置顶: `xcb_configure_window` + `XCB_STACK_MODE_ABOVE` - ---- - -## 四、组件管理层 - -### 4.1 WindowManager - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/WindowManager.h` | 窗口管理器 | -| `desktop/ui/components/WindowManager.cpp` | 实现 | - -#### 功能实现 - -- [x] 窗口注册/查询/关闭/置顶 -- [x] 仅持有弱引用 (`std::unordered_map>`) -- [x] 不拥有窗口对象生命周期 - -### 4.2 PanelManager - -#### 实现文件 - -| 文件路径 | 说明 | -|---------|------| -| `desktop/ui/components/PanelManager.h` | 面板管理器 | -| `desktop/ui/components/PanelManager.cpp` | 实现 | - -#### 功能实现 - -- [x] 边缘占用面板注册/注销 -- [x] 复杂布局算法: 按边缘分组 → 优先级排序 → 从边缘向内堆叠 → 计算可用几何 -- [x] 信号: `availableGeometryChanged()` - -### 4.3 ShellLayer / IShellLayer / IShellLayerStrategy - -- [x] Shell 层抽象接口 (与 QWidget 解耦) -- [x] 策略模式: 可插拔的 Shell 行为 (`IShellLayerStrategy`) -- [x] QWidget 实现: `ShellLayer` - -### 4.4 IPanel / IStatusBar - -- [x] 边缘面板抽象接口 (`IPanel`) -- [x] 状态栏接口 (`IStatusBar`) — 桩实现 - -### 4.5 DisplayServerBackendFactory - -- [x] 工厂模式创建平台特定后端 -- [x] 使用 `StaticRegisteredFactory` 静态注册 - ---- - -## 五、平台策略层 - -### 5.1 显示尺寸策略 - -- [x] `IDesktopDisplaySizeStrategy` — 策略模式控制窗口行为 -- [x] `WindowsDisplaySizePolicy` — Windows 实现 -- [x] `WSLDisplaySizePolicy` — WSL/X11 实现 - -### 5.2 属性策略 - -- [x] `IDesktopPropertyStrategy` — 属性策略基类 -- [x] `DesktopPropertyStrategyFactory` — 策略工厂 -- [x] 平台检测和策略选择 - -### 5.3 平台辅助 - -- [x] `platform_helper` — 平台工具 -- [x] `display_backend_helper` — 后端抽象 -- [x] `qt_backend` — Qt 后端检测 (X11/Wayland/EGLFS/LinuxFB) - ---- - -## 六、启动界面 Widget - -### 6.1 BootProgressWidget - -- [x] 抽象启动进度显示基类 -- [x] 工厂模式: `GetBootWidget(BootstrapStyle)` — Simple/Perfect/SelfDefined - -### 6.2 SimpleBootWidget - -- [x] 现代简约启动画面 -- [x] Logo + 品牌展示 -- [x] 分步进度指示器 (StepDotWidget) -- [x] Material Design 风格进度条 -- [x] 背景动画 (渐变色偏移) -- [x] 版本/版权信息 - -### 6.3 StepDotWidget - -- [x] 自定义步骤点绘制 -- [x] 三态: Inactive (灰色) / Active (紫色) / Completed (紫色 + 勾) - ---- - -## 七、渲染后端抽象 - -### 7.1 RenderBackend 接口 (设计完成,实现待开发) - -- [x] `render_backend.h` — 抽象接口 -- [x] `backend_capabilities.h` — 能力标志 -- [x] `render_backend_factory.h` — 工厂 (环境变量 → 平台检测 → 默认值) - -#### 待实现 - -- [ ] WindowsBackend (Windows 桌面渲染) -- [ ] EGLFSBackend (嵌入式 OpenGL ES) -- [ ] WaylandBackend (可选) -- [ ] X11Backend (可选) - ---- - -## 八、平台后端进度总结 - -| 平台 | 显示后端 | 窗口后端 | 渲染后端 | 状态 | -|------|---------|---------|---------|------| -| Windows | ✅ Win32 DWM | ✅ HWND + Hook | ❌ 待实现 | ✅ 可用 | -| Linux/WSL X11 | ✅ XCB + XWayland | ✅ XCB + QSocketNotifier | ❌ 待实现 | ✅ 可用 | -| Wayland | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | -| EGLFS (嵌入式) | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | -| LinuxFB | ❌ 未开始 | ❌ 未开始 | ❌ 待实现 | ⬜ 待开发 | - ---- - -## 九、关键架构模式 - -| 模式 | 应用 | -|------|------| -| 工厂模式 | DisplayServerBackendFactory, RenderBackendFactory, PlatformFactory | -| 策略模式 | IShellLayerStrategy, IDesktopDisplaySizeStrategy, IDesktopPropertyStrategy | -| 代理模式 | CFDesktopProxy, CFDesktopWindowProxy | -| 单例模式 | CFDesktopEntity | -| 弱引用模式 | WeakPtr 贯穿所有窗口和后端引用 | -| 观察者模式 | Qt Signal/Slot 事件链 | - ---- - -## 十、运行时平台检测链 - -优先级从高到低: - -1. `CFDESKTOP_DISPLAY_SERVER` 环境变量强制模式 -2. `WAYLAND_DISPLAY` 存在 → Client 模式 -3. `DISPLAY` 存在 → Client 模式 -4. `/dev/dri/card*` 存在 → DirectRender (EGLFS) -5. `/dev/fb0` 存在 → DirectRender (LinuxFB) -6. 默认 → Client 模式 - -编译时选择: -- Windows: CMake 自动包含 `windows/` 目录 -- Linux/WSL: CMake 自动包含 `linux_wsl/` 目录,检测 XCB 依赖 - ---- - -*最后更新: 2026-03-30* -*基于: 7 Agent 全面扫描结果* \ No newline at end of file diff --git a/document/todo/done/99_ui_material_framework_status.md b/document/todo/done/99_ui_material_framework_status.md deleted file mode 100644 index 853489c9c..000000000 --- a/document/todo/done/99_ui_material_framework_status.md +++ /dev/null @@ -1,629 +0,0 @@ ---- -title: "Phase 6: UI Material 框架 - 状态文档" -description: "模块ID: Phase 6,状态: 🚧 P1 控件完成,核心架构缺失" ---- - -# Phase 6: UI Material 框架 - 状态文档 - -> **模块ID**: Phase 6 -> **状态**: 🚧 P1 控件完成,核心架构缺失 -> **总体进度**: 75% -> **最后更新**: 2026-03-27 - ---- - -## 一、架构概览 - -### 1.1 分层架构 - -```text -Layer 6: Performance Profile (0%) -Layer 5: Material Widget Adapter (P0: 100%, P1: 100%, P2: 0%, P3: 0%) -Layer 4: Material Behavior Layer (100%) -Layer 3: Animation Engine Layer (90%) - 缺Rotate/Path动画 -Layer 2: Theme Engine Layer (100%) -Layer 1: Core Math & Utility Layer (100%) -Layer 0: Layout System (0%) - **新增:布局系统完全缺失** -```cpp - -### 1.2 设计规则 (RULE-01 to RULE-09) - -| 规则 | 描述 | -|------|------| -| **RULE-01** | Inheritance Only - 所有 Material 控件必须继承 Qt 原生控件 | -| **RULE-02** | Signal Preservation - 不移除/修改 Qt 公共信号 | -| **RULE-03** | Complete Paint Override - paintEvent 必须完全控制渲染,不使用 `style()->drawControl()` | -| **RULE-04** | No QSS - Qt StyleSheet 禁止用于 Material 视觉效果 | -| **RULE-05** | No Qt Quick - 不使用 QML/Qt Quick | -| **RULE-06** | Animation Disable - 动画系统必须支持全局禁用 | -| **RULE-07** | Theme Read-Only - 控件不能修改全局主题状态 | -| **RULE-08** | Component Injection - 渲染组件通过组合注入,而非直接 `new` | -| **RULE-09** | Token Access - 通过上下文感知接口和 token 字符串访问主题 | - -### 1.3 层级依赖规则 - -```cpp -ui/widget/material -> ui/components, ui/core, ui/base -ui/components -> ui/core, ui/base -ui/core -> ui/base -ui/base -> QtCore only (no QtWidgets/QtGui) -```bash - ---- - -## 二、完成状态总览 - -| Layer | 状态 | 完成度 | -|-------|------|--------| -| Layer 1: Core Math & Utility | ✅ | 100% | -| Layer 2: Theme Engine | ✅ | 100% | -| Layer 3: Animation Engine | ⚠️ | 90% | -| Layer 4: Material Behavior | ✅ | 100% | -| Layer 5: P0 核心控件 | ✅ | 100% (7/7) | -| Layer 5: P1 常用控件 | ✅ | 100% (12/12) | -| Layer 5: P2 高级控件 | ⬜ | 0% (0/27) | -| Layer 5: P3 专业控件 | ⬜ | 0% (0/25) | -| Layer 6: Performance Profile | ⬜ | 0% | -| Layer 0: Layout System | ❌ | 0% (新增) | - ---- - -## 三、已完成工作 - -### 3.1 Layer 1: Core Math & Utility (100%) - -| 文件 | 路径 | 功能 | -|------|------|------| -| MathHelper | `ui/base/math_helper.h` | 插值、钳位、曲线求值 | -| Color | `ui/base/color.h` | 颜色空间转换、HCT支持 | -| ColorHelper | `ui/base/color_helper.h` | 颜色混合、对比度 | -| Easing | `ui/base/easing.h` | Material缓动曲线 | -| GeometryHelper | `ui/base/geometry_helper.h` | 圆角形状生成 | -| DevicePixel | `ui/base/device_pixel.h` | DPI单位转换 | - -**核心特性:** -- 零 Qt UI 依赖 (仅 QtCore) -- 全面的动画和渲染数学函数 -- Material Color System 支持 -- 编译时哈希函数 (FNV-1a) - -### 3.2 Layer 2: Theme Engine (100%) - -#### Token 系统架构 - -```bash -Reference Token (MD3 spec) - | - v -System Token (cfmaterial scheme) - | - v -Component Token (widget-specific) -```text - -#### 已完成文件 - -**核心接口:** -- `ui/core/theme_manager.h` - 主题管理器单例 -- `ui/core/color_scheme.h` - 颜色方案接口 -- `ui/core/font_type.h` - 字体类型接口 -- `ui/core/motion_spec.h` - 动画规范接口 -- `ui/core/radius_scale.h` - 形状缩放接口 - -**Material 实现:** -- `ui/core/material/cfmaterial_scheme.h` - Material 颜色实现 -- `ui/core/material/cfmaterial_theme.h` - Material 主题 -- `ui/core/material/cfmaterial_fonttype.h` - Material 字体实现 -- `ui/core/material/cfmaterial_motion.h` - Material 动画实现 -- `ui/core/material/cfmaterial_radius_scale.h` - Material 形状实现 -- `ui/core/material/material_factory.h` - Material 主题工厂 - -**Token 目录:** (参见 ui/core/token/ 源码) - -### 3.3 Layer 3: Animation Engine (100%) - -#### 已完成文件 - -**核心接口:** -- `ui/components/animation.h` - ICFAbstractAnimation 基类 -- `ui/components/timing_animation.h` - ICFTimingAnimation (基于时间) -- `ui/components/animation_factory_manager.h` - 动画工厂管理器 - -**Material 实现:** -- `ui/components/material/cfmaterial_animation_factory.h` - Material 工厂实现 -- `ui/components/material/cfmaterial_animation_strategy.h` - 动画策略模式 -- `ui/components/material/cfmaterial_fade_animation.h` - 淡入淡出 -- `ui/components/material/cfmaterial_scale_animation.h` - 缩放变换 -- `ui/components/material/cfmaterial_slide_animation.h` - 滑动动画 -- `ui/components/material/cfmaterial_property_animation.h` - Qt 属性动画包装器 - -**Tokens:** -- `ui/components/material/token/animation_token_literals.h` - 动画 token 定义 -- `ui/components/material/token/animation_token_mapping.h` - Token 到动画映射 - -#### 动画状态 - -```cpp -Idle -> Running -> (Paused | Finished) - | - v - Stopped -```bash - -#### 支持的 Token 类型 -- `md.animation.fadeIn` -- `md.animation.fadeOut` -- `md.animation.slideUp` -- `md.animation.slideDown` -- `md.animation.scaleUp` -- `md.animation.scaleDown` -- `md.animation.rotateIn` -- `md.animation.rotateOut` - -### 3.4 Layer 4: Material Behavior (100%) - -| 组件 | 文件 | 功能 | -|------|------|------| -| StateMachine | `ui/widget/material/base/state_machine.h` | 视觉状态管理 | -| PainterLayer | `ui/widget/material/base/painter_layer.h` | 颜色叠加层绘制 | -| RippleHelper | `ui/widget/material/base/ripple_helper.h` | 涟漪效果控制器 | -| ElevationController | `ui/widget/material/base/elevation_controller.h` | 阴影/海拔系统 | -| FocusRing | `ui/widget/material/base/focus_ring.h` | 焦点环渲染器 | - -#### 状态机状态与透明度 - -| 状态 | 透明度 | 描述 | -|------|:------:|------| -| Normal | 0.00 | 默认状态 | -| Hovered | 0.08 | 鼠标悬停 | -| Pressed | 0.12 | 鼠标按下 | -| Focused | 0.12 | 键盘焦点 | -| Dragged | 0.16 | 正在拖拽 | -| Checked | 0.08 | 选中状态 | -| Disabled | 0.00 | 禁用状态 | - -**优先级:** Disabled > Pressed > Dragged > Focused > Hovered > Normal - -#### 海拔级别 - -| 级别 | DP | 阴影 (浅色) | 色调叠加 (深色) | -|------:|---:|-------------|------------------| -| 0 | 0dp | 无 | 无 | -| 1 | 1dp | blur=2px, offset=1px | overlay 1 | -| 2 | 3dp | blur=4px, offset=2px | overlay 2 | -| 3 | 6dp | blur=8px, offset=4px | overlay 3 | -| 4 | 8dp | blur=12px, offset=6px | overlay 4 | -| 5 | 12dp | blur=16px, offset=8px | overlay 5 | - -### 3.5 Layer 5: P0 核心控件 (100%) - -| 控件 | 文件 | Qt 基类 | 功能 | -|------|------|---------|------| -| **Button** | `ui/widget/material/widget/button/` | QPushButton | 5种变体、阴影、涟漪 | -| **TextField** | `ui/widget/material/widget/textfield/` | QLineEdit | 浮空标签、前后图标、计数器 | -| **TextArea** | `ui/widget/material/widget/textarea/` | QTextEdit | 多行文本输入 | -| **Label** | `ui/widget/material/widget/label/` | QLabel | 文本显示 | -| **CheckBox** | `ui/widget/material/widget/checkbox/` | QCheckBox | 复选框 | -| **RadioButton** | `ui/widget/material/widget/radiobutton/` | QRadioButton | 单选按钮 | -| **GroupBox** | `ui/widget/material/widget/groupbox/` | QGroupBox | 分组容器 | - -#### Button 变体 -- Filled (primary) - 填充主按钮 -- Outlined (secondary) - 轮廓次按钮 -- Text (tertiary) - 文本三级按钮 -- Tonal - 色调按钮 -- Elevated - 抬升按钮 - -#### TextField 功能 -- 2种变体:Filled, Outlined -- 浮空标签 -- 前后图标支持 -- 清除按钮 -- 字符计数器 -- 帮助文本/错误文本 -- 密码模式 - ---- - -## 四、待完成工作 - -### 4.1 Layer 5: P1 常用控件 (12/12) ✅ - -| 控件 | Qt 基类 | 路径 | Demo | -|------|---------|------|------| -| **Button** | QPushButton | `button/` | ButtonDemo | -| **CheckBox** | QCheckBox | `checkbox/` | CheckBoxDemo | -| **ComboBox** | QComboBox | `comboBox/` | ComboBoxDemo | -| **DoubleSpinBox** | QDoubleSpinBox | `doublespinbox/` | DoubleSpinBoxDemo | -| **GroupBox** | QGroupBox | `groupbox/` | GroupBoxDemo | -| **Label** | QLabel | `label/` | LabelDemo | -| **ListView** | QListView | `listview/` | ListViewDemo | -| **ProgressBar** | QProgressBar | `progressbar/` | ProgressBarDemo | -| **RadioButton** | QRadioButton | `radiobutton/` | RadioButtonDemo | -| **ScrollView** | QScrollArea | `scrollview/` | ScrollViewDemo | -| **Separator** | QFrame | `separator/` | SeparatorDemo | -| **Slider** | QSlider | `slider/` | SliderDemo | -| **SpinBox** | QSpinBox | `spinbox/` | SpinBoxDemo | -| **Switch** | QCheckBox | `switch/` | SwitchDemo | -| **TabView** | QTabWidget | `tabview/` | TabViewDemo | -| **TableView** | QTableView | `tableview/` | TableViewDemo | -| **TextArea** | QTextEdit | `textarea/` | TextAreaDemo | -| **TextField** | QLineEdit | `textfield/` | TextFieldDemo | -| **TreeView** | QTreeView | `treeview/` | TreeViewDemo | - -### 4.2 Layer 5: P2 高级控件 (0/27) - -| 控件 | Qt 基类 | 优先级 | -|------|---------|--------| -| DatePicker | QCalendarWidget | P2 | -| TimePicker | QTimeEdit | P2 | -| ColorPicker | QColorDialog | P2 | -| MenuBar | QMenuBar | P2 | -| ContextMenu | QMenu | P2 | -| ToolBar | QToolBar | P2 | -| StatusBar | QStatusBar | P2 | -| Dialog | QDialog | P2 | -| Snackbar | QWidget (custom) | P2 | -| Card | QFrame (custom) | P2 | -| FloatingActionButton | QPushButton (custom) | P2 | -| BottomNavigation | QWidget (custom) | P2 | -| Drawer | QWidget (custom) | P2 | -| SearchBar | QLineEdit (custom) | P2 | -| Dial | QDial | P2 | -| Stepper | QWidget (custom) | P2 | -| Chip | QWidget (custom) | P2 | -| Badge | QWidget (custom) | P2 | -| Tooltip | QToolTip | P2 | -| Popover | QWidget (custom) | P2 | -| CircularProgressBar | QWidget (custom) | P2 | -| NavigationRail | QWidget (custom) | P2 | -| NavigationDrawer | QWidget (custom) | P2 | -| TopAppBar | QWidget (custom) | P2 | -| BottomSheet | QWidget (custom) | P2 | -| AlertDialog | QWidget (custom) | P2 | -| Menu | QWidget (custom) | P2 | - -### 4.3 Layer 5: P3 专业控件 (0/25) - -| 控件 | 类型 | -|------|------| -| SplitView | 专业分割视图 | -| ChartView | 图表视图 | -| DrawingArea | 绘图区域 | -| Canvas | 画布控件 | -| FileList | 文件列表 | -| FileTree | 文件树 | -| CodeEditor | 代码编辑器 | -| Calendar | 日历控件 | -| Wizard | 向导对话框 | -| PropertyEditor | 属性编辑器 | -| TreeTable | 树表控件 | -| Gauge | 仪表盘 | -| Timeline | 时间轴 | -| GridView | 网格视图 | -| SwipeableItem | 可滑动项 | -| NotificationList | 通知列表 | -| Breadcrumbs | 面包屑导航 | -| Pagination | 分页控件 | -| RatingBar | 评分条 | -| RangeSlider | 范围滑块 | -| SliderCaptcha | 滑块验证码 | -| InputValidator | 输入验证器 | -| AutoComplete | 自动完成 | -| DropZone | 拖放区域 | -| RichTextToolBar | 富文本工具栏 | - -### 4.4 Layer 6: 性能配置 (0%) - -#### PerformanceProfile 枚举 - -```cpp -enum class PerformanceProfile { - Desktop, // 完整: 60fps, 阴影, 涟漪, 全部动画 - Embedded, // 降级: 30fps, 无阴影, 无涟漪, 仅状态动画 - UltraLow // 最小: 无动画, 无阴影, 仅颜色 -}; -```bash - -#### 降级策略 - -| 特性 | Desktop | Embedded | UltraLow | -|------|:-------:|:--------:|:--------:| -| 涟漪效果 | 是 | 否 | 否 | -| 阴影 | 是 | 否 | 否 | -| 状态动画 | 是 | 是 | 否 | -| 焦点动画 | 是 | 是 | 否 | -| 目标 FPS | 60 | 30 | N/A | - ---- - -## 五、实现规范 - -### 5.1 Widget 实现模板 - -所有控件遵循 Button 实现模式: - -```cpp -class Button : public QPushButton { - Q_OBJECT - -public: - explicit Button(QWidget* parent = nullptr); - void paintEvent(QPaintEvent* event) override; - QSize sizeHint() const override; - -protected: - void enterEvent(QEnterEvent* event) override; - void leaveEvent(QEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - -private: - StateMachine* m_stateMachine; - RippleHelper* m_ripple; - MdElevationController* m_elevation; - MdFocusIndicator* m_focusIndicator; -}; -```text - -### 5.2 事件处理模式 - -```cpp -void Widget::enterEvent(QEnterEvent* event) { - QPushButton::enterEvent(event); // 始终先调用父类 - m_stateMachine->onHoverEnter(); - update(); -} -```text - -### 5.3 标准绘制模式 - -```cpp -void Widget::paintEvent(QPaintEvent* event) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // 1. 背景 - painter.fillRect(rect(), backgroundColor()); - - // 2. 状态层 - m_stateMachine->paintStateLayer(&painter, rect()); - - // 3. 内容 - paintContent(&painter); - - // 4. 海拔阴影 - m_elevation->paintShadow(&painter, rect()); - - // 5. 焦点环 - if (hasFocus()) { - m_focusIndicator->paint(&painter, rect()); - } -} -```bash - ---- - -## 六、测试覆盖情况 - -### 已实现的测试 - -**ui/base 测试:** -- `math_helper_test.cpp` -- `color_test.cpp` -- `color_helper_test.cpp` -- `device_pixel_test.cpp` -- `geometry_helper_test.cpp` -- `easing_test.cpp` - -**ui/components 测试:** -- `state_machine_test.cpp` -- `ripple_helper_test.cpp` -- `elevation_controller_test.cpp` -- `focus_ring_test.cpp` -- `painter_layer_test.cpp` - -**ui/core 测试:** -- `token_test.cpp` - -### 关键缺口: UI控件测试 - -| 测试类型 | 状态 | 覆盖率 | -|---------|------|-------| -| UI Base 测试 | ✅ | 60% | -| UI Components 测试 | ✅ | 60% | -| UI Widgets 测试 | ❌ | **0%** | -| Widget 单元测试 | ❌ | **0%** | - -**急需补充**: 为19个P0/P1控件添加单元测试 -- button_test.cpp, textfield_test.cpp, textarea_test.cpp -- checkbox_test.cpp, radiobutton_test.cpp, groupbox_test.cpp -- label_test.cpp, slider_test.cpp, progressbar_test.cpp -- switch_test.cpp, combobox_test.cpp, spinbox_test.cpp -- doublespinbox_test.cpp, listview_test.cpp, treeview_test.cpp -- tableview_test.cpp, tabview_test.cpp, scrollview_test.cpp -- separator_test.cpp - ---- - -## 七、关键文件路径 - -### 已实现文件 - -**基础层 (Layer 1):** -```text -d:\ProjectHome\CFDesktop\ui\base\ -├── math_helper.h/cpp -├── color.h/cpp -├── color_helper.h/cpp -├── easing.h/cpp -├── geometry_helper.h/cpp -└── device_pixel.h/cpp -```text - -**核心层 (Layer 2):** -```text -d:\ProjectHome\CFDesktop\ui\core\ -├── theme_manager.h/cpp -├── color_scheme.h -├── material/ -│ ├── cfmaterial_scheme.h/cpp -│ ├── cfmaterial_theme.h/cpp -│ ├── cfmaterial_fonttype.h/cpp -│ ├── cfmaterial_motion.h/cpp -│ ├── cfmaterial_radius_scale.h/cpp -│ └── material_factory.h/cpp -└── token/ - ├── material_scheme/ - ├── motion/ - ├── radius_scale/ - ├── typography/ - └── theme_name/ -```text - -**动画层 (Layer 3):** -```text -d:\ProjectHome\CFDesktop\ui\components\ -├── animation.h/cpp -├── timing_animation.h/cpp -├── animation_factory_manager.h/cpp -└── material/ - ├── cfmaterial_animation_factory.h/cpp - ├── cfmaterial_animation_strategy.h/cpp - ├── cfmaterial_fade_animation.h/cpp - ├── cfmaterial_scale_animation.h/cpp - ├── cfmaterial_slide_animation.h/cpp - ├── cfmaterial_property_animation.h/cpp - └── token/ -```text - -**行为层 (Layer 4):** -```text -d:\ProjectHome\CFDesktop\ui\widget\material\base\ -├── state_machine.h/cpp -├── painter_layer.h/cpp -├── ripple_helper.h/cpp -├── elevation_controller.h/cpp -└── focus_ring.h/cpp -```text - -**控件层 (Layer 5 - P0 + P1):** -```text -/home/charliechen/CFDesktop/ui/widget/material/widget/ -├── button/button.h/cpp ✅ -├── textfield/textfield.h/cpp ✅ -├── textarea/textarea.h/cpp ✅ -├── label/label.h/cpp ✅ -├── checkbox/checkbox.h/cpp ✅ -├── radiobutton/radiobutton.h/cpp ✅ -├── groupbox/groupbox.h/cpp ✅ -├── combobox/combobox.h/cpp ✅ (P1) -├── slider/slider.h/cpp ✅ (P1) -├── progressbar/progressbar.h/cpp ✅ (P1) -├── switch/switch.h/cpp ✅ (P1) -├── listview/listview.h/cpp ✅ (P1) -├── treeview/treeview.h/cpp ✅ (P1) -├── tableview/tableview.h/cpp ✅ (P1) -├── tabview/tabview.h/cpp ✅ (P1) -├── scrollview/scrollview.h/cpp ✅ (P1) -├── separator/separator.h/cpp ✅ (P1) -├── spinbox/spinbox.h/cpp ✅ (P1) -└── doublespinbox/doublespinbox.h/cpp ✅ (P1) -```text - -### 待创建文件 - -**Layer 5 - P2 高级控件 (27个):** -```text -ui/widget/material/widget/ -├── datepicker/ # 日期选择器 -├── timepicker/ # 时间选择器 -├── colorpicker/ # 颜色选择器 -├── menubar/ # 菜单栏 -├── contextmenu/ # 右键菜单 -├── toolbar/ # 工具栏 -├── statusbar/ # 状态栏 -├── dialog/ # 对话框 -├── snackbar/ # 消息条 -├── card/ # 卡片 -├── floatingactionbutton/ # 浮动操作按钮 -├── bottomnavigation/ # 底部导航 -├── drawer/ # 抽屉 -├── searchbar/ # 搜索栏 -├── dial/ # 旋钮 -├── stepper/ # 步进器 -├── chip/ # 芯片 -├── badge/ # 徽章 -├── tooltip/ # 工具提示 -├── popover/ # 弹出框 -├── circularprogressbar/ # 圆形进度条 -├── navigationrail/ # 侧边导航 -├── navigationdrawer/ # 导航抽屉 -├── topappbar/ # 顶栏 -├── bottomsheet/ # 底部面板 -├── alertdialog/ # 警告对话框 -└── menu/ # 菜单 -```text - -**Layer 6 - 性能配置:** -```text -d:\ProjectHome\CFDesktop\ui\core\ -└── performance_profile.h/cpp -```bash - ---- - -## 八、P1 控件完成里程碑 (2026-03-19) - -### 完成概览 -- **完成日期**: 2026-03-18 -- **组件总数**: 19 个 (P0: 7 + P1: 12) -- **Demo 示例**: 19 个完整示例 -- **代码行数**: ~15000 行 (估算) - -### P1 新增组件详情 - -| 组件 | 主要功能 | 特性 | -|------|---------|------| -| **Slider** | 滑块输入 | 水平/垂直、刻度标记、轨道激活态 | -| **Switch** | 开关切换 | 动画滑块、52x32dp 尺寸、轨道颜色过渡 | -| **ProgressBar** | 进度显示 | 确定/不确定模式、平滑动画 | -| **ComboBox** | 下拉选择 | Filled/Outlined 变体、自定义弹出窗口 | -| **ListView** | 列表显示 | 单/双/三行项、自定义委托、分割线 | -| **TableView** | 表格显示 | Material 表头、网格线、行选择涟漪 | -| **TreeView** | 树形显示 | 展开/折叠动画、树连接线、56dp/级缩进 | -| **TabView** | 标签页 | 滑动指示器动画、关闭按钮、选项卡滚动 | -| **ScrollView** | 滚动视图 | 12dp 滚动条、淡入淡出、悬停扩展 | -| **Separator** | 分隔线 | 水平/垂直、FullBleed/Inset/MiddleInset | -| **SpinBox** | 整数输入 | 增量/减量按钮、轮廓样式、焦点指示器 | -| **DoubleSpinBox** | 浮点输入 | 同 SpinBox,支持浮点数 | - -### 共同特性实现 -- ✅ Material Design 3 色彩系统 (CFColor + MD3 tokens) -- ✅ 状态机管理 (hover/press/focus/disabled) -- ✅ 涟漪效果 (RippleHelper) -- ✅ 焦点指示器 (FocusRing) -- ✅ 动画支持 (CFMaterialAnimationFactory) -- ✅ 7步绘制流水线 - -### 构建集成 -所有组件已集成到 CMake 构建系统: -- `ui/widget/CMakeLists.txt` 已更新 -- `example/ui/widget/material_example/CMakeLists.txt` 已更新 -- MaterialGalleryWindow 已注册所有 Demo - -### 下一步工作 -1. **P2 高级控件** (27个) - 菜单、对话框、工具栏等 -2. **单元测试补充** - 提高 P0/P1 控件测试覆盖率 -3. **Layer 6: 性能配置** - HWTier 联动降级策略 - ---- - -## 九、相关文档 - -- 原始TODO: [../base/99_ui_material_framework.md](../base/99_ui_material_framework.md) - ---- - -*文档版本: v1.1* -*生成时间: 2026-03-19* diff --git a/document/todo/done/PROJECT_STATUS_REPORT.md b/document/todo/done/PROJECT_STATUS_REPORT.md deleted file mode 100644 index 7549d2908..000000000 --- a/document/todo/done/PROJECT_STATUS_REPORT.md +++ /dev/null @@ -1,515 +0,0 @@ ---- -title: CFDesktop 项目状态报告 -description: "报告日期: 2026-03-30,报告版本: v4.0" ---- - -# CFDesktop 项目状态报告 - -> **报告日期**: 2026-03-30 -> **报告版本**: v4.0 -> **项目版本**: 0.13.1 -> **扫描方式**: 7个并发Agent全面扫描 - ---- - -## 执行摘要 - -CFDesktop 是一个基于 Qt6 的嵌入式桌面框架项目,采用 Material Design 3 设计规范。项目使用 C++23 开发,旨在为嵌入式 Linux 设备提供完整的 UI 框架和开发工具链。 - -### 整体完成度: 约 75% - -| 模块 | 完成度 | 状态 | 优先级 | -|------|--------|------|--------| -| Phase 0: 工程骨架 | 100% | ✅ 已完成并归档 | P0 | -| Phase 1: 硬件探针 | 100% | ✅ 完成 | P0 | -| Phase 2: Base库核心 | 85% | 🚧 进行中 | P0 | -| Phase 3: 输入抽象层 | 0% | ⬜ 待开始 | P1 | -| Phase 4: 多平台模拟器 | 0% | ⬜ 待开始 | P1 | -| Phase 5: 测试体系 | 55% | 🚧 进行中 | P0 | -| Phase 6: UI框架 | 95% | 🚧 进行中 | P0 | -| Desktop 模块 | 90% | 🚧 进行中 | P0 | -| 显示后端 (Windows+WSL X11) | 70% | ✅ 核心完成 | P0 | -| 显示后端 (Wayland/嵌入式) | 0% | ⬜ 待开始 | P2 | - ---- - -## 一、各模块详细状态 - -### 1.0 工程骨架 (Phase 0) - 100% ✅ 已完成 - -#### ✅ 已完成 -- **CMake 构建系统** (100%) - - 多模块分层架构 (base/ui/desktop/example/test) - - C++23 标准 - - Qt 6.8.3 集成 - - 跨平台工具链配置 (Windows/LLVM、Windows/MSVC、Linux/GCC) - - 自动化 MOC/RCC/UIC - - VSCode 配置自动生成 (clangd、launch、tasks) - - 分类输出目录管理 - -- **代码规范配置** (100%) - - .clang-format 配置 - - .clangd 配置 - - Git pre-commit hook - -- **CI/CD** (100%) - - Git Hooks 本地验证策略 - - Docker 多架构构建镜像 - - MkDocs 文档自动部署 - ---- - -### 1.1 硬件探针 (Phase 1) - 100% ✅ 完成 - -#### ✅ 已完成 -- **CPU 检测器** (100%) - `base/system/cpu/` 模块 - - CPU型号、架构、制造商获取 - - CPU特性标志检测 (neon、aes、avx2等) - - 缓存大小查询 (L1、L2、L3) - - Big.LITTLE架构检测 - - 核心数量统计 - - CPU温度监测 - - 当前/最大频率查询 - - CPU使用率百分比 - - 跨平台支持 (Windows/Linux) - -- **内存检测器** (95%) - `base/system/memory/` 模块 - - 物理内存信息 (总内存、可用内存、空闲内存) - - 虚拟内存/交换空间信息 - - 进程内存信息 (RSS、虚拟内存、峰值) - - 内存模块(DIMM)信息 (容量、类型、频率、制造商) - - Linux特定缓存内存信息 - - 跨平台支持 (Windows/Linux) - -- **设备控制台** (85%) - `base/device/console/` 模块 - - 控制台尺寸查询 - - 颜色支持检测 - - 策略链模式实现容错机制 - -#### ⚠️ 部分完成 -- **策略链功能** (80%) - `base/include/base/policy_chain/` - - 核心PolicyChain类实现 - - 工厂函数和构建器API - - 完整的测试覆盖 - -#### ✅ 已完成 (新增) -- **GPU 检测器** (90%) - `base/system/gpu/` 模块 - - GPU设备信息、显示信息、环境评分 - - Linux DRM/WSL2检测、Windows DXGI检测 -- **网络检测器** (85%) - `base/system/network/` 模块 - - 网卡枚举、IP地址、可达性检测 - -#### ✅ 已完成 (HWTier 系统) -- **HWTier 系统** (100%) - `base/system/hardware_tier/` 模块 - - `HardwareTierLevel` 枚举 (Unknown/Low/Mid/High) - - 可插拔四阶段管线 (Collect→Score→Assess→Policy) - - 默认收集器、四维度评分器 (CPU/GPU/Memory/Display 各 0-100) - - 默认评估器和策略引擎 - - DeviceConfig 覆盖支持 (`setDeviceConfigOverride()`) - - `HardwareTierCapabilities` 能力标志 (OpenGL/软件渲染/动画/硬件解码/EGLFS/LinuxFB) - - 完整管线注册 API (`registerCollector/Scorer/Assessor/Policy`) - - 示例程序 `example/base/system/example_hardware_tier.cpp` - -#### ❌ 待完成 -- 自定义检测脚本执行支持 - ---- - -### 1.2 Base库核心 (Phase 2) - 80% 🚧 进行中 - -#### ✅ 已完成 -- **基础工具类** (100%) - - FNV-1a 哈希算法 - - Optional/Expected 类型 - - 单例模式 - - Scope Guard - - 弱指针实现 - - 跨平台宏定义 - - 无锁队列 (MPSC) - -- **配置管理系统** (85%) - `desktop/base/config_manager/` - - 四层配置存储系统 (Temp/App/User/System) - - 结构化键值管理 (KeyView, Key) - - 配置变更监控 (Watcher 机制) - - 异步/同步持久化 - - 类型安全的查询接口 - - 单例模式实现 - -- **日志系统** (90%) - `desktop/base/logger/` - - 多等级日志支持 (TRACE, DEBUG, INFO, WARNING, ERROR) - - 格式化日志函数 - - 异步日志队列 - - 多种输出格式化器 (console, file, default) - - 多种输出目标 (console sink, file sink) - - 线程安全的日志实现 - -- **DPI基础转换** (60%) - `ui/base/device_pixel.h` - - CanvasUnitHelper (dp/sp/px转换) - - BreakPoint响应式断点 (Compact/Medium/Expanded) - -#### ❌ 待完成 -- **DPI自动检测** (0%) - 自动获取显示器DPI -- **DPI自适应缩放** (0%) - 根据设备DPI自动调整UI -- 配置版本控制 -- 配置迁移机制 -- 配置验证功能 -- 网络日志输出器 -- 日志轮转功能 - ---- - -### 1.3 输入抽象层 (Phase 3) - 0% ⬜ 待开始 - -#### ❌ 完全未实现 -所有组件待实现: -- InputManager - 统一分发层 -- TouchInputHandler - 触摸处理器 -- KeyInputHandler - 按键处理器 -- RotaryInputHandler - 旋钮处理器 -- GestureRecognizer - 手势识别器 -- FocusNavigator - 焦点导航器 -- 原生设备驱动 (EvdevDevice, GPIOButton, RotaryEncoder) - ---- - -### 1.4 多平台模拟器 (Phase 4) - 0% ⬜ 待开始 - -#### ❌ 完全未实现 -需要实现: -- SimulatorWindow - 主窗口 -- DeviceFrame - 设备外壳 -- TouchVisualizer - 触摸可视化 -- HWTierSelector - 档位选择器 -- DPI 注入器 -- 硬件 Mock -- 输入模拟器 - ---- - -### 1.5 测试体系 (Phase 5) - 55% 🚧 进行中 - -#### ✅ 已完成 (测试覆盖率约55%) -- **测试框架** (100%) - - Google Test 集成 - - CMake 辅助函数 - - 跨平台测试脚本 - -- **base 模块测试** (90%) - - expected, scope_guard, hash, weak_ptr - - policy_chain - - mpsc_queue - -- **ui 模块测试** (60%) - - 基础工具 (color, math_helper, easing, geometry, device_pixel) - - Material组件 (state_machine, ripple, elevation, focus_ring, painter_layer) - - token_test - -- **system 模块测试** (70%) - - CPU信息查询测试 - - 内存信息查询测试 - -- **logger 模块测试** (85%) - - benchmark_test, error_handling_test, concurrency_test, formatter_test - -- **config_manager 模块测试** (80%) - - config_store_test - -#### ❌ 待完成 (覆盖率缺口约45%) -- **UI控件测试** (0%) - **关键缺口** - - 19个P0/P1控件全部无单元测试 - - button_test.cpp, textfield_test.cpp, checkbox_test.cpp, etc. - -- **desktop 模块** (5%) - - ASCII Art测试 (0%) - - 文件操作测试 (0%) - - desktop/base/ascii_art - - desktop/base/file_operations - -- **ui 模块缺口** - - UI核心功能 (theme, theme_manager) - -- **example 模块** - - GUI示例程序测试 - - 主题演示测试 - ---- - -### 1.6 UI Material Framework (Phase 6) - 75% 🚧 进行中 - -#### ✅ Layer 1-4 完成 100% -- **Layer 1**: Core Math & Utility (100%) - - math_helper, color, easing, geometry_helper, device_pixel - -- **Layer 2**: Theme Engine Layer (100%) - - ThemeManager, ThemeFactory, Token 系统 - - Material Color Scheme, Material Theme - - ColorScheme, FontType, MaterialScheme - - MaterialFactory, MaterialMotion, RadiusScale - -- **Layer 3**: Animation Engine Layer (90%) - - AnimationFactory, AnimationFactoryManager - - TimingAnimation, AnimationGroup - - Material动画策略 - - Material动画工厂 - - Fade/Slide/Scale动画 - - ⚠️ Rotate/Path动画缺失 - -- **Layer 4**: Material Behavior Layer (100%) - - StateMachine, RippleHelper, ElevationController - - FocusRing, PainterLayer - -#### ⚠️ 缺失部分 -- **布局管理系统** (0%) - Grid、Stack、ConstraintLayout -- **手势识别系统** (0%) - 触摸/手势统一接口 - -#### ✅ Layer 5 控件层 (P0-P1 完成 100%) -- **P0 核心控件** (7/7 完成 ✅) - - Button, TextField, TextArea, Label, CheckBox, RadioButton, GroupBox - -- **P1 常用控件** (12/12 完成 ✅) - - ComboBox, Slider, ProgressBar, Switch, ListView, TreeView, TableView, TabView, ScrollView, Separator, SpinBox, DoubleSpinBox - -- **P2 高级控件** (0/27 待实现) - - DatePicker, TimePicker, ColorPicker, MenuBar, ContextMenu, ToolBar, StatusBar, Dialog, Snackbar, Card, FloatingActionButton, BottomNavigation, Drawer, SearchBar, Dial, Stepper, Chip, Badge, Tooltip, Popover, CircularProgressBar 等 - -- **P3 专业控件** (0/25 待实现) - - SplitView, ChartView, DrawingArea, Canvas, FileList, FileTree, CodeEditor, Calendar, Wizard, PropertyEditor, TreeTable, Gauge, Timeline, GridView, SwipeableItem, BottomSheet, NotificationList, Breadcrumbs, Pagination, RatingBar, RangeSlider 等 - -#### ✅ 应用支持 (100%) -- Application, MaterialApplication - ---- - -### 1.7 Desktop 模块 - 90% 🚧 进行中 - -#### ✅ 已完成 -- **主入口程序** (85%) - `desktop/main/` - - 主入口程序 (main.cpp) - - DAG 初始化链 (InitSessionChain) — Kahn 算法拓扑排序 - - 早期配置阶段 (early_config_stage.cpp) - - 日志系统启动阶段 (logger_stage.cpp) - - 欢迎界面实现 (early_welcome_impl.cpp) - - GUI 初始化阶段 (gui_init_stage.cpp) - - 路径解析器 (path/) - -- **ASCII Art 模块** (100%) - `desktop/base/ascii_art/` - - 核心 Canvas 类 - - 四种边框样式 (Single, Double, Rounded, Solid) - - UTF-8 支持 - - 高级文本处理 - -- **文件操作模块** (70%) - `desktop/base/file_operations/` - - 文件/目录创建 - - 路径拼接 - - 路径存在检查 - - 应用运行时目录获取 - -- **显示后端架构** (90%) - `desktop/ui/components/` + `desktop/ui/platform/` - - IDisplayServerBackend 三模式抽象 (Client/Compositor/DirectRender) - - IWindowBackend 窗口后端接口 - - IWindow 平台无关窗口接口 - - WindowManager 窗口管理器 (弱引用模式) - - PanelManager 面板管理器 (边缘布局算法) - - ShellLayer / IShellLayer / IShellLayerStrategy - - DisplayServerBackendFactory 工厂 - -- **Windows 平台后端** (100%) - `desktop/ui/platform/windows/` - - WindowsDisplayServerBackend (Win32 DWM) - - WindowsWindowBackend (SetWinEventHook + EnumWindows) - - WindowsWindow (HWND → IWindow 适配) - - WindowsDisplaySizePolicy / WindowsPropertyStrategy - -- **WSL X11 平台后端** (100%) - `desktop/ui/platform/linux_wsl/` - - WSLX11DisplayServerBackend (XCB + XWayland) - - WSLX11WindowBackend (XCB + QSocketNotifier) - - WSLX11Window (xcb_window_t → IWindow 适配) - - WSLDisplaySizePolicy / WSLPlatformFactory - -- **启动界面 Widget** (100%) - `desktop/ui/widget/init_session/` - - SimpleBootWidget (Logo + 分步进度 + Material 风格) - - StepDotWidget (三态步骤点) - - BootWidgetFactory - -- **桌面核心类** (90%) - `desktop/ui/` - - CFDesktopEntity (单例,全局桌面生命周期) - - CFDesktop (主桌面 Widget) - - CFDesktopProxy / CFDesktopWindowProxy (代理模式) - -- **渲染后端抽象** (30%) - `desktop/ui/render/` - - RenderBackend 接口设计 - - BackendCapabilities 能力标志 - - RenderBackendFactory 工厂 - - ❌ 具体实现未开发 - -#### ❌ 待完成 -- Wayland 后端实现 -- EGLFS / LinuxFB 嵌入式后端 -- RenderBackend 具体实现 -- ReleaseEarlyInit() 函数 -- file_operations 高级功能 (权限/监控/加密) - ---- - -### 1.8 文档状态 - 60% 🚧 进行中 - -#### ✅ 已完成文档 (约153个Markdown文件) -- **架构设计文档** (高质量) - - 工程骨架 (Phase 0) - - 开发环境配置 - - CI/CD 流程 - - UI Material Design 层文档 - - 日志系统文档 (7篇) - -- **API 文档** (完整) - - CPU 检测 API - - 内存检测 API - - 基础工具类 API - - UI 框架 API - - Material 组件 API - -- **开发工具文档** - - Git Hooks 规范 - - Docker 构建指南 - - 代码格式化工具 - - 测试框架文档 - -#### ❌ 缺失文档 -- **核心基础模块** - - HardwareProbe 完整文档 - - CapabilityPolicy 策略引擎文档 - - ConfigStore 配置中心文档 - - InputLayer 输入抽象层文档 - -- **UI 控件文档** (大量缺失) - - P1 控件文档 (12个) - - P2 控件文档 (27个) - - P3 控件文档 (25个) - -- **模拟器和工具** - - Simulator 多平台模拟器文档 - - PerformanceProfile 性能优化文档 - -- **集成和部署** - - 应用打包指南 - - SDK 导出文档 - - 多架构部署文档 - -- **高级主题** - - 性能优化指南 - - 自定义主题开发 - - 国际化支持 - - 无障碍访问 - ---- - -### 1.9 示例代码状态 - 50% 🚧 进行中 - -#### ✅ 已有示例 -- **base 模块** (4个) - - example_policy_chain.cpp - - example_console_helper.cpp - - example_cpu_info.cpp - - example_memory_info.cpp - -- **desktop 模块** (4个) - - config_manager/example_usage.cpp - - logger/example_usage.cpp - - material_gallery (GUI示例) - - 主题相关示例 (4个) - -- **ui 模块** (19个控件演示) - - 所有 P0 控件演示 - - 所有 P1 控件演示 - -#### ❌ 缺失示例 -- **base 模块** - - hash/FNV-1a 哈希算法示例 - - lockfree/无锁队列示例 - - scope_guard 示例 - - singleton 示例 - - span 示例 - - weak_ptr 示例 - - expected 示例 - -- **desktop 模块** - - ascii_art 示例 - - file_operations 示例 - - main 主程序启动流程示例 - -- **ui 模块** - - core/ 核心UI机制示例 - - components/ 通用组件示例 - - models/ 数据模型示例 - ---- - -## 二、技术栈总结 - -| 组件 | 版本/技术 | -|------|----------| -| 语言 | C++23 | -| 框架 | Qt 6.8.3 | -| 构建 | CMake 3.16+ + Ninja | -| 测试 | Google Test | -| 文档 | MkDocs + Doxygen | -| 平台支持 | Windows, Linux, macOS | -| 架构支持 | x86_64, ARM64, ARMhf | - ---- - -## 三、风险识别 - -| 风险 | 级别 | 影响 | 缓解措施 | -|------|------|------|----------| -| UI控件测试覆盖率为0% | 高 | 质量风险 | 为P0/P1控件添加测试 | -| DPI自动检测缺失 | 中 | 用户体验 | 实现DPI自动检测和自适应 | -| ReleaseEarlyInit()未实现 | 中 | 启动流程不完整 | 实现完整初始化链 | -| 布局系统缺失 | 中 | UI功能受限 | 实现Grid/Stack布局 | -| P2/P3 UI控件未实现 | 中 | 功能完整性 | 按需实现 | -| 文档更新滞后 | 中 | 维护困难 | 同步更新文档 | - ---- - -## 四、关键文件路径 - -```text -CFDesktop/ -├── CMakeLists.txt # 主构建配置 (v0.13.1, C++23) -├── base/ # 基础库模块 -│ ├── system/cpu/ # ✅ CPU检测 (100%) -│ ├── system/memory/ # ✅ 内存检测 (95%) -│ ├── system/gpu/ # ✅ GPU检测 (90%) -│ ├── system/network/ # ✅ 网络检测 (85%) -│ ├── system/hardware_tier/ # ✅ HWTier 硬件分级 (100%) -│ ├── device/console/ # ✅ 控制台设备 (85%) -│ └── include/base/policy_chain/ # ✅ 策略链 (80%) -├── ui/ # UI框架 (95%) -│ ├── base/ # ✅ 基础工具 (100%) -│ │ └── device_pixel.h/cpp # ✅ DPI基础转换 (60%) -│ ├── core/ # ✅ 主题核心 (100%) -│ ├── components/ # ⚠️ 动画组件 (90%) -│ └── widget/material/ # ✅ P0/P1控件 (100%) -├── desktop/ # 桌面环境 (90%) -│ ├── main/ # 🚧 主程序入口 (85%) -│ ├── base/logger/ # ✅ 日志系统 (90%) -│ ├── base/config_manager/ # ✅ 配置管理 (85%) -│ ├── base/ascii_art/ # ✅ ASCII艺术 (100%) -│ ├── base/file_operations/ # ⚠️ 文件操作 (70%) -│ └── ui/ # ✅ Desktop UI (90%) -│ ├── components/ # ✅ 显示后端抽象+窗口管理 -│ ├── platform/windows/ # ✅ Windows后端 (100%) -│ ├── platform/linux_wsl/ # ✅ WSL X11后端 (100%) -│ ├── render/ # ⚠️ 渲染后端 (30%, 接口only) -│ └── widget/init_session/ # ✅ 启动Widget (100%) -├── test/ # 测试代码 (55%覆盖率, 24个测试) -├── example/ # 示例程序 (70%覆盖, 80个示例) -└── document/ # 文档 (80%覆盖, 271篇) -```yaml - ---- - -*报告生成时间: 2026-03-30* -*报告版本: v4.0* -*扫描方式: 7个并发Agent全面扫描* -*项目路径: /home/charliechen/CFDesktop* -*项目版本: 0.13.1* diff --git a/document/todo/done/SUMMARY.md b/document/todo/done/SUMMARY.md new file mode 100644 index 000000000..c734c5fb2 --- /dev/null +++ b/document/todo/done/SUMMARY.md @@ -0,0 +1,29 @@ +--- +title: 已完成阶段归档 +description: CFDesktop 各已完成阶段的历史状态汇总。详细报告已归档,当前事实来源请参阅 status/current.md。 +--- + +# 已完成阶段归档 + +各阶段的历史状态报告已归档。当前项目状态的唯一事实来源是 [status/current.md](../../status/current.md)。 + +## 阶段完成汇总 + +| 阶段 | 模块 | 状态 | 归档报告 | +|------|------|------|----------| +| Phase 0 | 工程骨架搭建 | ✅ 完成 | ~~已归档~~ | +| Phase 1 | 硬件探针与能力分级 | ✅ 完成 | ~~已归档~~ | +| Phase 2 | 基础库核心 | ✅ 完成 | ~~已归档~~ | +| Phase A | 基础设施 (CI/CD) | ✅ 完成 | ~~已归档~~ | +| Phase 6 | UI Material 框架 | ✅ 完成 | ~~已归档~~ | +| Phase G | Widget 应用与示例 | ✅ 完成 | ~~已归档~~ | +| Phase H | 显示后端与窗口管理 | ✅ 完成 | ~~已归档~~ | +| Milestone 1 | 桌面骨架可见 | ✅ 完成 | ~~已归档~~ | + +## 进行中 / 待开始阶段 + +参见 [TODO 索引](../index.md) 和 [当前状态](../../status/current.md)。 + +--- + +*归档时间: 2026-05-23* diff --git a/document/todo/done/index.md b/document/todo/done/index.md deleted file mode 100644 index bbb0e7e1a..000000000 --- a/document/todo/done/index.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: done -description: 已完成模块的状态文档索引,本目录记录 CFDesktop 各模块的完成状态和实现细节。 ---- - -# done - -> 已完成模块的状态文档索引 - -## Overview - -本目录记录 CFDesktop 各模块的完成状态和实现细节。 - -## 状态文档列表 - -| 文件 | 模块 | 完成度 | 最后更新 | -|------|------|--------|----------| -| [00_project_skeleton_status.md](00_project_skeleton_status.md) | Phase 0: 工程骨架 | ✅ 100% | 2026-03-11 | -| [01_hardware_probe_status.md](01_hardware_probe_status.md) | Phase 1: 硬件探针 | 🚧 90% | 2026-03-21 | -| [02_base_library_status.md](02_base_library_status.md) | Phase 2: Base库核心 | ✅ 100% | - | -| [03_input_layer_status.md](03_input_layer_status.md) | Phase 3: 输入抽象层 | ⬜ 0% | - | -| [04_simulator_status.md](04_simulator_status.md) | Phase 4: 多平台模拟器 | ⬜ 0% | - | -| [05_testing_status.md](05_testing_status.md) | Phase 5: 测试体系 | 🚧 55% | - | -| [06_infrastructure_status.md](06_infrastructure_status.md) | Phase A: 基础设施 (GPU/Network/Config/Logger) | 🚧 50% | 2026-03-21 | -| [13_widget_apps_status.md](13_widget_apps_status.md) | Phase G: Widget 应用与示例 | ✅ 100% | 2026-03-21 | -| [99_ui_material_framework_status.md](99_ui_material_framework_status.md) | Phase 6: UI Material Framework | 🚧 95% | - | -| [14_display_backend_status.md](14_display_backend_status.md) | Phase H: 显示后端与窗口管理 | 🚧 70% | 2026-03-30 | - -## 综合报告 - -- **[PROJECT_STATUS_REPORT.md](PROJECT_STATUS_REPORT.md)** — v4.0, 项目版本 0.13.1 -- **整体完成度**: ~75% - ---- - -*Last updated: 2026-03-30* diff --git a/document/todo/done/milestone_01_desktop_skeleton.md b/document/todo/done/milestone_01_desktop_skeleton.md deleted file mode 100644 index d3c7c41ea..000000000 --- a/document/todo/done/milestone_01_desktop_skeleton.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: "Milestone 1: 桌面骨架可见" -description: "状态: ✅ 已完成 (2026-04-09)" ---- - -# Milestone 1: 桌面骨架可见 - -> **状态**: ✅ 已完成 (2026-04-09) -> **预计周期**: 1-2 天 -> **前置依赖**: 无 (当前基础设施已就绪) -> **目标**: 运行后不再是空白,能看到一个带背景的全屏桌面窗口 - ---- - -## 一、阶段目标 - -启动 CFDesktop 后,ShellLayer 渲染出桌面背景(纯色或壁纸),PanelManager 的可用区域计算正确,桌面主 Widget 正确显示。 - -**完成标志**: `./scripts/run.sh` 启动后看到全屏非空白的桌面画面。 - ---- - -## 二、当前状态分析 - -### 已有基础设施 - -| 组件 | 文件 | 状态 | -|------|------|------| -| `CFDesktop` (主 QWidget) | `desktop/ui/CFDesktop.h` | ✅ 已创建,`register_desktop_resources()` 可注入 PanelManager 和 ShellLayer | -| `CFDesktopEntity` (生命周期) | `desktop/ui/CFDesktopEntity.h/.cpp` | ✅ 单例,`run_init()` 已接入 DisplayBackend | -| `ShellLayer` | `desktop/ui/components/ShellLayer.h` | ✅ 接口+声明完整,**缺 .cpp 实现文件** | -| `IShellLayer` | `desktop/ui/components/IShellLayer.h` | ✅ `setStrategy()` + `geometry()` 接口 | -| `IShellLayerStrategy` | `desktop/ui/components/IShellLayerStrategy.h` | ✅ `activate(layer, wm)` + `deactivate()` + `onGeometryChanged()` | -| `PanelManager` | `desktop/ui/components/PanelManager.h/.cpp` | ✅ 完整实现,`registerPanel()` / `availableGeometry()` | -| `IPanel` | `desktop/ui/components/IPanel.h` | ✅ `position()` / `priority()` / `preferredSize()` / `widget()` | -| `SimpleBootWidget` | `desktop/ui/widget/init_session/simple_boot_widget.h/.cpp` | ✅ 启动画面已完整 | - -### 关键缺口 - -1. **ShellLayer.cpp 不存在** — 只有 `.h` 声明,没有实现 -2. **CFDesktop 没有 paintEvent** — 桌面主窗口不会画任何东西 -3. **没有壁纸/背景渲染逻辑** — IShellLayerStrategy 没有具体实现 -4. **CFDesktopEntity::run_init() 没有创建 ShellLayer** — 只做了 DisplayBackend 初始化 -5. **CFDesktop 没有被 show()** — 桌面主 Widget 可能从未显示 - ---- - -## 三、待实现任务 - -### Day 1: ShellLayer 实现与桌面背景渲染 - -#### Step 1: 创建 ShellLayer.cpp -- [x] 创建文件 `desktop/ui/components/ShellLayer.cpp` -- [x] 实现构造函数 `ShellLayer(QWidget* parent)` - - 设置 `setAttribute(Qt::WA_OpaquePaintEvent)` 减少闪烁 - - 设置 `setAutoFillBackground(false)` - - 接受 parent 的全尺寸 geometry -- [x] 实现 `setStrategy(std::unique_ptr strategy)` - - 若已有旧 strategy,先调用 `deactivate()` - - 存储新 strategy - - 调用 `activate(this->GetWeak(), wm_weak)` 激活 -- [x] 实现 `geometry()` — 返回 `QWidget::geometry()` -- [x] 实现 `onAvailableGeometryChanged(const QRect& rect)` - - 调用 `setGeometry(rect)` 调整 ShellLayer 尺寸 - - 转发给 `strategy_->onGeometryChanged(rect)` - -**参考已有接口**: -- `ShellLayer.h:35` — 类声明 -- `IShellLayer.h:37` — 接口定义 -- `IShellLayerStrategy.h:33` — 策略接口 - -#### Step 2: 创建 DefaultShellStrategy (桌面背景策略) -- [x] 创建文件 `desktop/ui/strategy/default_shell_strategy.h/.cpp` -- [x] 实现 `IShellLayerStrategy` 接口 -- [x] `activate()` 中: - - 持有 ShellLayer 的 weak 引用 - - 在 ShellLayer 上创建背景子 Widget 或直接绘制 -- [x] `onGeometryChanged()` 中: - - 更新背景区域尺寸 - - 触发重绘 -- [x] `deactivate()` 中:清理资源 - -#### Step 3: 为 ShellLayer 添加背景绘制 -- [x] 方案 A (推荐): 在 ShellLayer 的 `paintEvent()` 中绘制纯色背景 - ```cpp - void ShellLayer::paintEvent(QPaintEvent*) { - QPainter p(this); - auto* theme = ThemeManager::instance().currentTheme(); - p.fillRect(rect(), theme->colorScheme().surface()); - } - ``` -- [x] 方案 B: 壁纸图片渲染 - - 在 `DefaultShellStrategy::activate()` 中加载壁纸 QPixmap - - 提供 `WallpaperLayer` 子 Widget 或在 paintEvent 中 `drawPixmap()` -- [x] 选择配色:使用 ThemeManager 的 `colorScheme().surface()` 作为默认背景色 - -**可复用**: `ui/core/theme_manager.h` (ThemeManager 单例)、`ui/core/color_scheme.h` (颜色方案) - -#### Step 4: 修改 CFDesktopEntity::run_init() 连接所有组件 -- [x] 在 `CFDesktopEntity::run_init()` 中(文件 `desktop/ui/CFDesktopEntity.cpp:45`): - ```cpp - // 1. 创建 PanelManager - auto* panel_mgr = new PanelManager(desktop_entity_, desktop_entity_); - - // 2. 创建 ShellLayer - auto* shell = new ShellLayer(desktop_entity_); - - // 3. 注入到 CFDesktop - CFDesktop::InitResources res; - res.panel_manager_ = panel_mgr; - res.shell_layer_ = shell; - desktop_entity_->register_desktop_resources(res); - - // 4. 设置 ShellLayer 策略 - shell->setStrategy(std::make_unique()); - - // 5. 连接 PanelManager geometry 变化到 ShellLayer - connect(panel_mgr, &PanelManager::availableGeometryChanged, - shell, &ShellLayer::onAvailableGeometryChanged); - - // 6. 显示 - desktop_entity_->showFullScreen(); - ``` - -#### Step 5: 确保 CFDesktop 正确接收 geometry -- [x] 在 `CFDesktop::register_desktop_resources()` 中(文件 `desktop/ui/CFDesktop.cpp:21`): - - 验证 `panel_manager_` 和 `shell_layer_` 非空 - - 触发 PanelManager 初始布局计算 - -#### Step 6: 更新 CMakeLists.txt -- [x] 在 `desktop/ui/components/CMakeLists.txt` 中添加 `ShellLayer.cpp` -- [x] 如新建 strategy 目录,确保 CMake 能发现新文件 - ---- - -### Day 2: 验证与调试 - -#### Step 7: 构建并运行 -- [x] `cmake --build build` 确认编译通过 -- [x] 运行后验证: - - [x] 桌面全屏显示 - - [x] 背景非空白(纯色或壁纸) - - [x] 关闭后无 crash / 内存泄漏 - -#### Step 8: 调试布局 -- [x] 在 PanelManager::relayout() 中添加 debug log 输出 `availableGeometry()` -- [x] 确认 ShellLayer 的 geometry 正确跟随屏幕大小 -- [x] 确认后续注册 Panel 后 availableGeometry 会正确缩小 - ---- - -## 四、关键文件清单 - -### 需要修改的文件 -| 文件 | 修改内容 | -|------|----------| -| `desktop/ui/CFDesktopEntity.cpp` | `run_init()` 中创建 ShellLayer、PanelManager、设置策略、show | -| `desktop/ui/components/CMakeLists.txt` | 添加 ShellLayer.cpp | - -### 需要新建的文件 -| 文件 | 内容 | -|------|------| -| `desktop/ui/components/ShellLayer.cpp` | ShellLayer 实现 | -| `desktop/ui/strategy/default_shell_strategy.h` | 默认 Shell 策略头文件 | -| `desktop/ui/strategy/default_shell_strategy.cpp` | 默认 Shell 策略实现 | - -### 参考文件 (只读) -| 文件 | 用途 | -|------|------| -| `desktop/ui/CFDesktop.h:76-81` | `InitResources` 结构体 | -| `desktop/ui/CFDesktop.cpp:21-25` | `register_desktop_resources()` | -| `desktop/ui/components/PanelManager.h` | PanelManager 接口 | -| `desktop/ui/components/IShellLayerStrategy.h` | 策略接口 | -| `desktop/ui/widget/init_session/simple_boot_widget.cpp` | QPainter 绘制参考 | -| `ui/core/theme_manager.h` | ThemeManager 单例 | - ---- - -## 五、验收标准 - -- [x] `./scripts/run.sh` 后看到全屏非空白桌面 -- [x] 桌面背景色跟随 ThemeManager 的 surface 色值 -- [x] PanelManager 的 `availableGeometry()` 返回正确值 (初始为全屏) -- [x] 后续注册 Panel 后 `availableGeometry()` 会自动缩小 -- [x] 关闭后无内存泄漏 - ---- - -*最后更新: 2026-03-31* diff --git a/document/todo/index.md b/document/todo/index.md index 4617709b3..8903b9e18 100644 --- a/document/todo/index.md +++ b/document/todo/index.md @@ -1,22 +1,32 @@ --- -title: CFDesktop 项目 TODO 索引 -description: "- 综合报告: PROJECTSTATUSREPORT.md" +title: CFDesktop 项目 TODO 看板 +description: 本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于设计文档和架构规范整理而成。 --- -# CFDesktop 项目 TODO 索引 +# CFDesktop 项目 TODO 看板 -## 快速导航 +本目录包含 CFDesktop 项目各模块的详细 TODO 清单,基于 `design_stage` 设计文档和 `MaterialRules.md` 架构规范整理而成。 -| 模块 | TODO 文件 | 参考文档 | 状态 | -|------|----------|----------|------| -| 工程骨架 | ~~已归档~~ | [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | ✅ 100% | -| 硬件探针 | ~~已归档~~ | [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 🚧 90% | -| ~~Base库~~ | ~~已归档~~ | [done/02_base_library_status.md](done/02_base_library_status.md) | ✅ 100% | -| 输入层 | [02_input_layer.md](base/02_input_layer.md) | [done/03_input_layer_status.md](done/03_input_layer_status.md) | ⬜ 0% | -| 模拟器 | [03_simulator.md](base/03_simulator.md) | [done/04_simulator_status.md](done/04_simulator_status.md) | ⬜ 0% | -| 测试 | [04_testing.md](base/04_testing.md) | [done/05_testing_status.md](done/05_testing_status.md) | 🚧 55% | -| UI框架 | [99_ui_material_framework.md](base/99_ui_material_framework.md) | [done/99_ui_material_framework_status.md](done/99_ui_material_framework_status.md) | 🚧 95% | -| 显示后端 | desktop/ | [done/14_display_backend_status.md](done/14_display_backend_status.md) | 🚧 70% | +## 模块索引 + +| TODO 文件 | 模块 | 预计周期 | 依赖 | 状态 | +|----------|------|---------|------|------| +| [done/00_project_skeleton_status.md](done/00_project_skeleton_status.md) | 工程骨架搭建 | 1~2 周 | - | ✅ 100% | +| [done/01_hardware_probe_status.md](done/01_hardware_probe_status.md) | 硬件探针与能力分级 | 2~3 周 | Phase 0 | 🚧 90% | +| ~~[done/02_base_library_status.md](done/02_base_library_status.md)~~ | ~~Base 库核心~~ | ~~3~4 周~~ | Phase 0, 1 | ✅ 100% | +| [02_input_layer.md](base/02_input_layer.md) | 输入抽象层 | 1~2 周 | Phase 0, 1 | ⬜ 0% | +| [03_simulator.md](base/03_simulator.md) | 多平台模拟器 | 2~3 周 | Phase 0, 2 | ⬜ 0% | +| [04_testing.md](base/04_testing.md) | 测试体系 | 贯穿全程 | 所有阶段 | 🚧 55% | +| [99_ui_material_framework.md](base/99_ui_material_framework.md) | UI Material Framework | 持续迭代 | Phase 0-3 | 🚧 95% | +| desktop/ | Desktop 模块 (显示后端+窗口管理) | 持续迭代 | Phase 0-6 | 🚧 90% | + +## 状态图例 + +- ⬜ **待开始** (Todo) - 尚未开始的任务 +- 🚧 **进行中** (In Progress) - 正在开发的任务 +- ✅ **已完成** (Done) - 已完成的任务 +- ⚠️ **已废弃** (Deprecated) - 不再需要的任务 +- 🔄 **阻塞中** (Blocked) - 被依赖阻塞的任务 ## 项目状态报告 @@ -24,10 +34,6 @@ description: "- 综合报告: PROJECTSTATUSREPORT.md" - **整体完成度**: 约 75% - **显示后端详情**: [done/14_display_backend_status.md](done/14_display_backend_status.md) — Windows + WSL X11 后端已完成 -## 详细信息 - -请查看 [TODO 索引](./) 获取完整的 TODO 看板信息。 - --- *最后更新: 2026-03-30* diff --git a/scripts/README.md b/scripts/README.md index 319f089d7..4ac046a25 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -162,4 +162,6 @@ bash scripts/develop/remove_trailing_space.sh .\scripts\develop\remove_trailing_space.ps1 ``` +> 完整脚本文档: [document/scripts/](../document/scripts/) + diff --git a/scripts/lib/README.md b/scripts/lib/README.md index 7c82cd842..ad8e3c2d4 100644 --- a/scripts/lib/README.md +++ b/scripts/lib/README.md @@ -366,3 +366,5 @@ eval "$(get_ini_config "$CONFIG_FILE")" | 日志函数重复 | 25 次 | 1 次 | -96% | | 配置解析重复 | 15 次 | 1 次 | -93% | | 总重复代码 | ~2,400 行 | ~600 行 | -75% | + +> 完整库文档: [document/scripts/lib/](../../../document/scripts/lib/) diff --git a/scripts/release/hooks/README.md b/scripts/release/hooks/README.md index 3223d4706..58aaa502f 100644 --- a/scripts/release/hooks/README.md +++ b/scripts/release/hooks/README.md @@ -31,3 +31,5 @@ bash scripts/release/hooks/install_hooks.sh ## 详细文档 完整使用指南请参考: [document/release_rule/git_hooks_guide.md](../../../document/release_rule/git_hooks_guide.md) + +> 完整脚本文档: [document/scripts/release/hooks/](../../../document/scripts/release/hooks/) From ea1df7af4dd3b7c33c012a7120c0314a26a53c0c Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 24 May 2026 07:34:25 +0800 Subject: [PATCH 4/4] simplified: remove the old documentations --- document/status/current.md | 43 -------------------------------------- document/status/index.md | 10 --------- 2 files changed, 53 deletions(-) delete mode 100644 document/status/current.md delete mode 100644 document/status/index.md diff --git a/document/status/current.md b/document/status/current.md deleted file mode 100644 index d204d3fff..000000000 --- a/document/status/current.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: CFDesktop 当前状态 -description: "最后更新: 2026-05-22,- 本地 可发现 47 个 CTest 测试。" ---- - -# CFDesktop 当前状态 - -> 最后更新: 2026-05-22 - -## 基本信息 - -| 项目 | 当前值 | -|------|--------| -| 版本 | 0.18.0 | -| 语言标准 | C++23 | -| UI 框架 | Qt 6.8.x | -| 构建系统 | CMake + Docker build helpers | -| 文档系统 | VitePress | -| 当前开发分支 | develop | - -## 当前基线 - -- 本地 `out/build_develop/test` 可发现 47 个 CTest 测试。 -- 最近一次本地验证结果: 47/47 通过。 -- VitePress 文档构建命令 `pnpm build` 可通过。 -- 自动生成的 `document/api/**` 暂不纳入 VitePress 主站,避免旧 Doxybook2 生成页影响构建。 - -## 当前主线 - -短期建议优先推进“可见可用桌面闭环”: - -1. 状态栏 -2. 任务栏/导航栏 -3. 应用启动器 -4. 窗口管理可见联动 - -HWTier、InputManager、RenderBackend、Wayland/EGLFS 后端仍然重要,但当前不应阻塞桌面可演示版本。 - -## 已知需要收敛的地方 - -- 历史文档中仍有 `0.13.1`、C++17、MkDocs 等旧描述。 -- `document/todo/done/` 是历史状态归档,不应作为当前事实源。 -- API 自动文档二期需要重新选择发布方式:Doxygen HTML 独立发布,或修复 Doxybook2 Markdown 链接后再纳入主站。 diff --git a/document/status/index.md b/document/status/index.md deleted file mode 100644 index b5259660b..000000000 --- a/document/status/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 项目状态 -description: 本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历 ---- - -# 项目状态 - -本节记录 CFDesktop 的当前事实状态。后续开发和 AI 辅助分析应优先读取这里,而不是旧的历史状态报告。 - -- [当前状态](current.md)