综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

1107 строки
37 KiB

  1. #include "hmi_editor_service.h"
  2. #include "domain/hmi_control_registry.h"
  3. #include "project_service.h"
  4. #include "domain/project_limits.h"
  5. #include <algorithm>
  6. #include <cctype>
  7. #include <cstddef>
  8. #include <map>
  9. #include <optional>
  10. #include <set>
  11. #include <utility>
  12. namespace {
  13. // 仅在调用方提供错误字符串时写入失败原因
  14. void setError(std::string *error, const std::string &message)
  15. {
  16. if (error != nullptr)
  17. {
  18. *error = message;
  19. }
  20. }
  21. // 创建统一的 HMI 编辑失败结果
  22. HmiEditorResult failure(HmiEditorError error, const std::string &message)
  23. {
  24. return {false, error, message, {}};
  25. }
  26. // 返回控件绑定值连续占用的 D 字数量
  27. int hmiBindingWordCount(const HmiControl &control)
  28. {
  29. if (control.binding.has_value() && control.binding->area() == RegisterArea::M)
  30. {
  31. return 1;
  32. }
  33. if (control.type == HmiControlType::NumericDisplay
  34. || control.type == HmiControlType::NumericInput
  35. || (control.type == HmiControlType::StatusText
  36. && control.binding.has_value()
  37. && control.binding->area() == RegisterArea::D))
  38. {
  39. return registerDataTypeWordCount(control.dataType);
  40. }
  41. return 1;
  42. }
  43. // 在指定寄存器区域内查找控件可用的首个连续地址
  44. std::optional<RegisterAddress> nextHmiBindingAddress(
  45. const Project &project,
  46. const HmiControlDescriptor &descriptor,
  47. RegisterDataType data_type)
  48. {
  49. std::optional<RegisterArea> area = hmiBindingArea(descriptor.bindingKind);
  50. if (descriptor.bindingKind == HmiBindingKind::BitOrWord)
  51. {
  52. area = RegisterArea::M;
  53. }
  54. if (!area.has_value())
  55. {
  56. return std::nullopt;
  57. }
  58. int next_index = RegisterAddress::kMinimumIndex;
  59. for (const HmiPage &page : project.hmiPages)
  60. {
  61. for (const HmiControl &control : page.controls)
  62. {
  63. if (!control.binding.has_value()
  64. || control.binding->area() != *area)
  65. {
  66. continue;
  67. }
  68. next_index = std::max(
  69. next_index,
  70. control.binding->index() + hmiBindingWordCount(control));
  71. }
  72. }
  73. for (int index = next_index;
  74. index <= RegisterAddress::kMaximumIndex;
  75. ++index)
  76. {
  77. const RegisterAddress candidate{*area, index};
  78. if (*area == RegisterArea::D
  79. && !registerDataTypeAddressIsValid(data_type, candidate))
  80. {
  81. continue;
  82. }
  83. return candidate;
  84. }
  85. return std::nullopt;
  86. }
  87. // 判断页面名称是否只包含空白字符
  88. bool isBlank(const std::string &value)
  89. {
  90. return value.empty()
  91. || std::all_of(
  92. value.cbegin(), value.cend(),
  93. [](unsigned char character) { return std::isspace(character) != 0; });
  94. }
  95. // 从 page-1 开始递增生成工程内唯一的页面 ID
  96. std::string makeUniquePageId(const Project &project)
  97. {
  98. int suffix = 1;
  99. while (true)
  100. {
  101. const std::string candidate = "page-" + std::to_string(suffix);
  102. const bool found = std::any_of(
  103. project.hmiPages.cbegin(), project.hmiPages.cend(),
  104. [&candidate](const HmiPage &page) { return page.id == candidate; });
  105. if (!found)
  106. {
  107. return candidate;
  108. }
  109. ++suffix;
  110. }
  111. }
  112. // 统计当前工程全部 HMI 页面中的控件总数
  113. std::size_t totalControlCount(const Project &project)
  114. {
  115. std::size_t total = 0U;
  116. for (const HmiPage &page : project.hmiPages)
  117. {
  118. total += page.controls.size();
  119. }
  120. return total;
  121. }
  122. } // namespace
  123. // 保存工程服务和 HMI 默认配置的引用
  124. HmiEditorService::HmiEditorService(
  125. ProjectService &project_service,
  126. const HmiDefaultSettings &hmi_defaults)
  127. : project_service_(project_service),
  128. hmi_defaults_(hmi_defaults)
  129. {
  130. }
  131. // 深拷贝当前页面集合和初始页面 ID 作为历史快照
  132. HmiEditorService::HistoryState HmiEditorService::captureState() const
  133. {
  134. const Project &project = project_service_.project();
  135. return {project.hmiPages, project.initialHmiPageId};
  136. }
  137. // 仅在编辑确实改变工程状态时记录撤销快照
  138. void HmiEditorService::recordHistory(HistoryState before)
  139. {
  140. const HistoryState after = captureState();
  141. history_.record(std::move(before), after, &HmiEditorService::statesEqual);
  142. }
  143. // 比较两个完整 HMI 编辑快照是否一致
  144. bool HmiEditorService::statesEqual(
  145. const HistoryState &left, const HistoryState &right)
  146. {
  147. if (left.initial_page_id != right.initial_page_id
  148. || left.pages.size() != right.pages.size())
  149. {
  150. return false;
  151. }
  152. for (std::size_t index = 0; index < left.pages.size(); ++index)
  153. {
  154. if (!pagesEqual(left.pages[index], right.pages[index]))
  155. {
  156. return false;
  157. }
  158. }
  159. return true;
  160. }
  161. // 比较两个页面的基础属性和全部控件
  162. bool HmiEditorService::pagesEqual(
  163. const HmiPage &left, const HmiPage &right)
  164. {
  165. if (left.id != right.id || left.name != right.name
  166. || left.width != right.width || left.height != right.height
  167. || left.controls.size() != right.controls.size())
  168. {
  169. return false;
  170. }
  171. for (std::size_t index = 0; index < left.controls.size(); ++index)
  172. {
  173. if (!controlsEqual(left.controls[index], right.controls[index]))
  174. {
  175. return false;
  176. }
  177. }
  178. return true;
  179. }
  180. // 比较两个控件中所有会保存到工程的属性
  181. bool HmiEditorService::controlsEqual(
  182. const HmiControl &left, const HmiControl &right)
  183. {
  184. const bool page_jump_equal = left.pageJump.has_value() == right.pageJump.has_value()
  185. && (!left.pageJump.has_value()
  186. || left.pageJump->targetPageId == right.pageJump->targetPageId);
  187. return left.id == right.id
  188. && left.type == right.type
  189. && left.bounds.x == right.bounds.x
  190. && left.bounds.y == right.bounds.y
  191. && left.bounds.width == right.bounds.width
  192. && left.bounds.height == right.bounds.height
  193. && left.text == right.text
  194. && left.binding == right.binding
  195. && left.dataType == right.dataType
  196. && left.properties == right.properties
  197. && left.buttonOperation == right.buttonOperation
  198. && left.buttonEnableCondition == right.buttonEnableCondition
  199. && left.statusText == right.statusText
  200. && page_jump_equal;
  201. }
  202. // 创建撤销或重做不可执行时的统一失败结果
  203. HmiEditorResult HmiEditorService::historyFailure(const std::string &message)
  204. {
  205. return {false, HmiEditorError::InvalidOperation, message, {}};
  206. }
  207. // 遍历工程所有 HmiPage,根据页面 ID 查找页面,找不到返回空指针
  208. const HmiPage *HmiEditorService::findPage(const std::string &page_id) const
  209. {
  210. const Project &project = project_service_.project();
  211. const auto page = std::find_if(
  212. project.hmiPages.cbegin(),
  213. project.hmiPages.cend(),
  214. [&page_id](const HmiPage &candidate)
  215. {
  216. return candidate.id == page_id;
  217. });
  218. return page == project.hmiPages.cend() ? nullptr : &*page;
  219. }
  220. // 先找到页面 → 在页面内查找指定控件;页面不存在 / 控件不存在都返回 nullptr
  221. const HmiControl *HmiEditorService::findControl(
  222. const std::string &page_id, const std::string &control_id) const
  223. {
  224. const HmiPage *page = findPage(page_id);
  225. if (page == nullptr)
  226. {
  227. return nullptr;
  228. }
  229. const auto control = std::find_if(
  230. page->controls.cbegin(),
  231. page->controls.cend(),
  232. [&control_id](const HmiControl &candidate)
  233. {
  234. return candidate.id == control_id;
  235. });
  236. return control == page->controls.cend() ? nullptr : &*control;
  237. }
  238. // 获取第一个页面 ID;没有页面返回空字符串
  239. std::string HmiEditorService::firstPageId() const
  240. {
  241. const Project &project = project_service_.project();
  242. return project.hmiPages.empty() ? std::string{} : project.hmiPages.front().id;
  243. }
  244. // 保证工程至少存在一个 HMI 页面
  245. HmiEditorResult HmiEditorService::ensureDefaultPage()
  246. {
  247. if (!project_service_.project().hmiPages.empty())
  248. {
  249. if (project_service_.project().initialHmiPageId.empty())
  250. {
  251. Project &project = project_service_.editProject();
  252. project.initialHmiPageId = project.hmiPages.front().id;
  253. }
  254. return {true, HmiEditorError::None, {}, firstPageId()};
  255. }
  256. // 仅在首个控件操作前创建默认页面,空工程仍可正常保存
  257. HistoryState before = captureState();
  258. HmiPage page;
  259. page.id = "page-1";
  260. page.name = "主操作页面";
  261. page.width = hmi_defaults_.pageWidth;
  262. page.height = hmi_defaults_.pageHeight;
  263. Project &project = project_service_.editProject();
  264. project.hmiPages.push_back(std::move(page));
  265. project.initialHmiPageId = project.hmiPages.back().id;
  266. recordHistory(std::move(before));
  267. return {true, HmiEditorError::None, {}, project.hmiPages.back().id};
  268. }
  269. // 新建并校验一个页面,成功后记录单步撤销历史
  270. HmiEditorResult HmiEditorService::addPage(const std::string &name)
  271. {
  272. if (isBlank(name))
  273. {
  274. return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空");
  275. }
  276. const Project &current = project_service_.project();
  277. const ProjectLimitSettings &limits = project_service_.projectLimits();
  278. if (current.hmiPages.size() >= limits.maximumHmiPages)
  279. {
  280. return failure(
  281. HmiEditorError::InvalidPage,
  282. "当前配置最多允许 "
  283. + std::to_string(limits.maximumHmiPages) + " 个 HMI 页面");
  284. }
  285. if (name.size() > ProjectLimits::kMaximumTextBytes)
  286. {
  287. return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节");
  288. }
  289. const bool duplicate = std::any_of(
  290. current.hmiPages.cbegin(), current.hmiPages.cend(),
  291. [&name](const HmiPage &page) { return page.name == name; });
  292. if (duplicate)
  293. {
  294. return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一");
  295. }
  296. HmiPage page;
  297. page.id = makeUniquePageId(current);
  298. page.name = name;
  299. page.width = hmi_defaults_.pageWidth;
  300. page.height = hmi_defaults_.pageHeight;
  301. HistoryState before = captureState();
  302. Project &project = project_service_.editProject();
  303. project.hmiPages.push_back(std::move(page));
  304. if (project.initialHmiPageId.empty())
  305. {
  306. project.initialHmiPageId = project.hmiPages.back().id;
  307. }
  308. recordHistory(std::move(before));
  309. return {true, HmiEditorError::None, {}, project.hmiPages.back().id};
  310. }
  311. // 校验新尺寸不会让已有控件越界后调整页面大小
  312. HmiEditorResult HmiEditorService::resizePage(
  313. const std::string &page_id, int width, int height)
  314. {
  315. const HmiPage *page = findPage(page_id);
  316. if (page == nullptr)
  317. {
  318. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  319. }
  320. if (page->width == width && page->height == height)
  321. {
  322. return {true, HmiEditorError::None, {}, page_id};
  323. }
  324. // 先在副本上校验新页面边界,失败时不触碰当前工程
  325. HmiPage candidate = *page;
  326. candidate.width = width;
  327. candidate.height = height;
  328. std::string error;
  329. if (!candidate.validate(project_service_.projectLimits(), &error))
  330. {
  331. return failure(HmiEditorError::InvalidPage, error);
  332. }
  333. HistoryState before = captureState();
  334. Project &project = project_service_.editProject();
  335. auto editable = std::find_if(
  336. project.hmiPages.begin(), project.hmiPages.end(),
  337. [&page_id](const HmiPage &item) { return item.id == page_id; });
  338. editable->width = width;
  339. editable->height = height;
  340. recordHistory(std::move(before));
  341. return {true, HmiEditorError::None, {}, page_id};
  342. }
  343. // 校验名称非空且不重复后修改页面名称
  344. HmiEditorResult HmiEditorService::renamePage(
  345. const std::string &page_id, const std::string &name)
  346. {
  347. if (isBlank(name))
  348. {
  349. return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空");
  350. }
  351. if (name.size() > ProjectLimits::kMaximumTextBytes)
  352. {
  353. return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节");
  354. }
  355. const Project &current = project_service_.project();
  356. const auto existing = std::find_if(
  357. current.hmiPages.cbegin(), current.hmiPages.cend(),
  358. [&page_id](const HmiPage &page) { return page.id == page_id; });
  359. if (existing == current.hmiPages.cend())
  360. {
  361. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  362. }
  363. const bool duplicate = std::any_of(
  364. current.hmiPages.cbegin(), current.hmiPages.cend(),
  365. [&page_id, &name](const HmiPage &page)
  366. {
  367. return page.id != page_id && page.name == name;
  368. });
  369. if (duplicate)
  370. {
  371. return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一");
  372. }
  373. if (existing->name == name)
  374. {
  375. return {true, HmiEditorError::None, {}, page_id};
  376. }
  377. HistoryState before = captureState();
  378. Project &project = project_service_.editProject();
  379. auto page = std::find_if(
  380. project.hmiPages.begin(), project.hmiPages.end(),
  381. [&page_id](const HmiPage &candidate) { return candidate.id == page_id; });
  382. page->name = name;
  383. recordHistory(std::move(before));
  384. return {true, HmiEditorError::None, {}, page_id};
  385. }
  386. // 确认页面可删除且未被引用后从工程中移除页面
  387. HmiEditorResult HmiEditorService::removePage(const std::string &page_id)
  388. {
  389. const Project &current = project_service_.project();
  390. const auto page = std::find_if(
  391. current.hmiPages.cbegin(), current.hmiPages.cend(),
  392. [&page_id](const HmiPage &candidate) { return candidate.id == page_id; });
  393. if (page == current.hmiPages.cend())
  394. {
  395. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  396. }
  397. if (current.hmiPages.size() <= 1)
  398. {
  399. return failure(HmiEditorError::LastPageRequired, "工程至少需要保留一个 HMI 页面");
  400. }
  401. if (current.initialHmiPageId == page_id)
  402. {
  403. return failure(
  404. HmiEditorError::InitialPageCannotBeRemoved,
  405. "请先设置其他初始页面,再删除当前页面");
  406. }
  407. for (const HmiPage &source_page : current.hmiPages)
  408. {
  409. const auto reference = std::find_if(
  410. source_page.controls.cbegin(), source_page.controls.cend(),
  411. [&page_id](const HmiControl &control)
  412. {
  413. return control.type == HmiControlType::PageJump
  414. && control.pageJump.has_value()
  415. && control.pageJump->targetPageId == page_id;
  416. });
  417. if (reference != source_page.controls.cend())
  418. {
  419. return failure(
  420. HmiEditorError::PageReferenced,
  421. "页面仍被跳转控件 " + reference->id + " 引用");
  422. }
  423. }
  424. HistoryState before = captureState();
  425. Project &project = project_service_.editProject();
  426. project.hmiPages.erase(
  427. std::remove_if(
  428. project.hmiPages.begin(), project.hmiPages.end(),
  429. [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }),
  430. project.hmiPages.end());
  431. recordHistory(std::move(before));
  432. return {true, HmiEditorError::None, {}, page_id};
  433. }
  434. // 将指定页面在页面列表中上移或下移一位
  435. HmiEditorResult HmiEditorService::movePage(const std::string &page_id, int offset)
  436. {
  437. if (offset != -1 && offset != 1)
  438. {
  439. return failure(HmiEditorError::InvalidOperation, "页面每次只能上移或下移一位");
  440. }
  441. const Project &current = project_service_.project();
  442. const auto page = std::find_if(
  443. current.hmiPages.cbegin(), current.hmiPages.cend(),
  444. [&page_id](const HmiPage &candidate) { return candidate.id == page_id; });
  445. if (page == current.hmiPages.cend())
  446. {
  447. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  448. }
  449. const auto index = std::distance(current.hmiPages.cbegin(), page);
  450. const std::ptrdiff_t target_index = index + offset;
  451. if (target_index < 0
  452. || target_index >= static_cast<std::ptrdiff_t>(current.hmiPages.size()))
  453. {
  454. return failure(HmiEditorError::InvalidOperation, "HMI 页面已经位于目标边界");
  455. }
  456. HistoryState before = captureState();
  457. Project &project = project_service_.editProject();
  458. std::iter_swap(
  459. project.hmiPages.begin() + index,
  460. project.hmiPages.begin() + target_index);
  461. recordHistory(std::move(before));
  462. return {true, HmiEditorError::None, {}, page_id};
  463. }
  464. // 将指定页面设置为工程启动时显示的初始页面
  465. HmiEditorResult HmiEditorService::setInitialPage(const std::string &page_id)
  466. {
  467. if (findPage(page_id) == nullptr)
  468. {
  469. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  470. }
  471. if (project_service_.project().initialHmiPageId == page_id)
  472. {
  473. return {true, HmiEditorError::None, {}, page_id};
  474. }
  475. HistoryState before = captureState();
  476. project_service_.editProject().initialHmiPageId = page_id;
  477. recordHistory(std::move(before));
  478. return {true, HmiEditorError::None, {}, page_id};
  479. }
  480. // 根据控件注册描述创建控件并分配默认寄存器地址
  481. HmiEditorResult HmiEditorService::addControl(
  482. const std::string &page_id, HmiControlType type)
  483. {
  484. const HmiPage *page = findPage(page_id);
  485. if (page == nullptr)
  486. {
  487. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  488. }
  489. const ProjectLimitSettings &limits = project_service_.projectLimits();
  490. if (page->controls.size() >= limits.maximumHmiControlsPerPage)
  491. {
  492. return failure(
  493. HmiEditorError::InvalidControl,
  494. "当前配置下单个 HMI 页面最多允许 "
  495. + std::to_string(limits.maximumHmiControlsPerPage) + " 个控件");
  496. }
  497. if (totalControlCount(project_service_.project())
  498. >= ProjectLimits::kMaximumHmiControlsPerProject)
  499. {
  500. return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件");
  501. }
  502. const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type);
  503. if (descriptor == nullptr)
  504. {
  505. return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型");
  506. }
  507. HmiControl control = makeControl(*page, *descriptor);
  508. const std::optional<RegisterAddress> binding = nextHmiBindingAddress(
  509. project_service_.project(), *descriptor, control.dataType);
  510. if (descriptor->bindingKind != HmiBindingKind::None && !binding.has_value())
  511. {
  512. return failure(
  513. HmiEditorError::InvalidControl,
  514. "没有可用的 HMI 默认寄存器地址");
  515. }
  516. control.binding = binding;
  517. HistoryState before = captureState();
  518. Project &project = project_service_.editProject();
  519. auto target_page = std::find_if(
  520. project.hmiPages.begin(),
  521. project.hmiPages.end(),
  522. [&page_id](const HmiPage &candidate)
  523. {
  524. return candidate.id == page_id;
  525. });
  526. target_page->controls.push_back(std::move(control));
  527. recordHistory(std::move(before));
  528. return {true,
  529. HmiEditorError::None,
  530. {},
  531. target_page->controls.back().id};
  532. }
  533. // 通过批量删除入口删除单个控件
  534. HmiEditorResult HmiEditorService::removeControl(
  535. const std::string &page_id, const std::string &control_id)
  536. {
  537. return removeControls(page_id, {control_id});
  538. }
  539. // 完整校验所选控件后一次性删除,避免留下部分修改
  540. HmiEditorResult HmiEditorService::removeControls(
  541. const std::string &page_id,
  542. const std::vector<std::string> &control_ids)
  543. {
  544. if (findPage(page_id) == nullptr)
  545. {
  546. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  547. }
  548. if (control_ids.empty())
  549. {
  550. return failure(HmiEditorError::InvalidOperation, "请先选择要删除的 HMI 控件");
  551. }
  552. std::set<std::string> selected_ids;
  553. for (const std::string &control_id : control_ids)
  554. {
  555. if (!selected_ids.insert(control_id).second)
  556. {
  557. return failure(HmiEditorError::InvalidOperation, "删除列表中存在重复控件");
  558. }
  559. if (findControl(page_id, control_id) == nullptr)
  560. {
  561. return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件");
  562. }
  563. }
  564. HistoryState before = captureState();
  565. Project &project = project_service_.editProject();
  566. auto page = std::find_if(
  567. project.hmiPages.begin(),
  568. project.hmiPages.end(),
  569. [&page_id](const HmiPage &candidate)
  570. {
  571. return candidate.id == page_id;
  572. });
  573. page->controls.erase(
  574. std::remove_if(
  575. page->controls.begin(),
  576. page->controls.end(),
  577. [&selected_ids](const HmiControl &candidate)
  578. {
  579. return selected_ids.find(candidate.id) != selected_ids.end();
  580. }),
  581. page->controls.end());
  582. recordHistory(std::move(before));
  583. return {true, HmiEditorError::None, {}, control_ids.front()};
  584. }
  585. // 校验候选矩形后移动或缩放指定控件
  586. HmiEditorResult HmiEditorService::moveControl(
  587. const std::string &page_id,
  588. const std::string &control_id,
  589. const HmiRect &bounds)
  590. {
  591. const HmiPage *page = findPage(page_id);
  592. const HmiControl *control = findControl(page_id, control_id);
  593. if (page == nullptr)
  594. {
  595. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  596. }
  597. if (control == nullptr)
  598. {
  599. return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件");
  600. }
  601. // 先校验候选位置,失败时不修改工程模型
  602. HmiControl candidate = *control;
  603. candidate.bounds = bounds;
  604. // 更新前保留原控件,只有全部编辑规则通过才覆盖原值
  605. // 属性更新是整体替换:类型不变,但绑定、外观和扩展属性必须一起通过校验
  606. std::string error;
  607. if (!validateEditableControl(*page, candidate, &error))
  608. {
  609. return failure(HmiEditorError::InvalidControl, error);
  610. }
  611. if (control->bounds.x == bounds.x
  612. && control->bounds.y == bounds.y
  613. && control->bounds.width == bounds.width
  614. && control->bounds.height == bounds.height)
  615. {
  616. return {true, HmiEditorError::None, {}, control_id};
  617. }
  618. HistoryState before = captureState();
  619. Project &project = project_service_.editProject();
  620. auto target = std::find_if(
  621. project.hmiPages.begin(), project.hmiPages.end(),
  622. [&page_id](const HmiPage &item) { return item.id == page_id; });
  623. auto editable = std::find_if(
  624. target->controls.begin(), target->controls.end(),
  625. [&control_id](const HmiControl &item) { return item.id == control_id; });
  626. editable->bounds = bounds;
  627. recordHistory(std::move(before));
  628. return {true, HmiEditorError::None, {}, control_id};
  629. }
  630. // 按所选控件的整体范围批量计算并应用对齐位置
  631. HmiEditorResult HmiEditorService::alignControls(
  632. const std::string &page_id,
  633. const std::vector<std::string> &control_ids,
  634. HmiAlignment alignment)
  635. {
  636. const HmiPage *page = findPage(page_id);
  637. if (page == nullptr)
  638. {
  639. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  640. }
  641. if (control_ids.size() < 2U)
  642. {
  643. return failure(
  644. HmiEditorError::InvalidOperation,
  645. "请至少选择两个 HMI 控件");
  646. }
  647. std::set<std::string> selected_ids;
  648. for (const std::string &control_id : control_ids)
  649. {
  650. if (!selected_ids.insert(control_id).second)
  651. {
  652. return failure(
  653. HmiEditorError::InvalidOperation,
  654. "对齐列表中存在重复控件");
  655. }
  656. if (findControl(page_id, control_id) == nullptr)
  657. {
  658. return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件");
  659. }
  660. }
  661. const HmiControl *first = findControl(page_id, control_ids.front());
  662. int left = first->bounds.x;
  663. int top = first->bounds.y;
  664. int right = first->bounds.x + first->bounds.width;
  665. int bottom = first->bounds.y + first->bounds.height;
  666. for (std::size_t index = 1U; index < control_ids.size(); ++index)
  667. {
  668. const HmiControl *control = findControl(page_id, control_ids[index]);
  669. left = std::min(left, control->bounds.x);
  670. top = std::min(top, control->bounds.y);
  671. right = std::max(
  672. right, control->bounds.x + control->bounds.width);
  673. bottom = std::max(
  674. bottom, control->bounds.y + control->bounds.height);
  675. }
  676. bool supported = true;
  677. switch (alignment)
  678. {
  679. case HmiAlignment::Left:
  680. case HmiAlignment::HorizontalCenter:
  681. case HmiAlignment::Right:
  682. case HmiAlignment::Top:
  683. case HmiAlignment::VerticalCenter:
  684. case HmiAlignment::Bottom:
  685. break;
  686. default:
  687. supported = false;
  688. break;
  689. }
  690. if (!supported)
  691. {
  692. return failure(HmiEditorError::InvalidOperation, "不支持的 HMI 对齐方式");
  693. }
  694. HmiPage candidate_page = *page;
  695. const int horizontal_center = (left + right) / 2;
  696. const int vertical_center = (top + bottom) / 2;
  697. bool changed = false;
  698. for (HmiControl &control : candidate_page.controls)
  699. {
  700. if (selected_ids.find(control.id) == selected_ids.end())
  701. {
  702. continue;
  703. }
  704. const HmiRect original = control.bounds;
  705. switch (alignment)
  706. {
  707. case HmiAlignment::Left:
  708. control.bounds.x = left;
  709. break;
  710. case HmiAlignment::HorizontalCenter:
  711. control.bounds.x = horizontal_center - control.bounds.width / 2;
  712. break;
  713. case HmiAlignment::Right:
  714. control.bounds.x = right - control.bounds.width;
  715. break;
  716. case HmiAlignment::Top:
  717. control.bounds.y = top;
  718. break;
  719. case HmiAlignment::VerticalCenter:
  720. control.bounds.y = vertical_center - control.bounds.height / 2;
  721. break;
  722. case HmiAlignment::Bottom:
  723. control.bounds.y = bottom - control.bounds.height;
  724. break;
  725. default:
  726. break;
  727. }
  728. changed = changed || original.x != control.bounds.x
  729. || original.y != control.bounds.y;
  730. }
  731. if (!changed)
  732. {
  733. return {
  734. true,
  735. HmiEditorError::None,
  736. {},
  737. control_ids.front()};
  738. }
  739. std::string error;
  740. if (!candidate_page.validate(project_service_.projectLimits(), &error))
  741. {
  742. return failure(HmiEditorError::InvalidControl, error);
  743. }
  744. const HistoryState before = captureState();
  745. Project &project = project_service_.editProject();
  746. auto target_page = std::find_if(
  747. project.hmiPages.begin(), project.hmiPages.end(),
  748. [&page_id](const HmiPage &item) { return item.id == page_id; });
  749. for (const HmiControl &candidate : candidate_page.controls)
  750. {
  751. if (selected_ids.find(candidate.id) == selected_ids.end())
  752. {
  753. continue;
  754. }
  755. auto editable = std::find_if(
  756. target_page->controls.begin(), target_page->controls.end(),
  757. [&candidate](const HmiControl &item)
  758. {
  759. return item.id == candidate.id;
  760. });
  761. editable->bounds = candidate.bounds;
  762. }
  763. recordHistory(before);
  764. return {true, HmiEditorError::None, {}, control_ids.front()};
  765. }
  766. // 校验并整体更新控件的全部可编辑属性
  767. HmiEditorResult HmiEditorService::updateControl(
  768. const std::string &page_id,
  769. const std::string &control_id,
  770. const HmiControl &control)
  771. {
  772. const HmiPage *page = findPage(page_id);
  773. const HmiControl *existing = findControl(page_id, control_id);
  774. if (page == nullptr)
  775. {
  776. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  777. }
  778. if (existing == nullptr)
  779. {
  780. return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件");
  781. }
  782. if (control.type != existing->type)
  783. {
  784. return failure(HmiEditorError::InvalidControl, "不能修改已创建 HMI 控件的类型");
  785. }
  786. std::string error;
  787. if (!validateEditableControl(*page, control, &error))
  788. {
  789. return failure(HmiEditorError::InvalidControl, error);
  790. }
  791. if (hasDuplicateControlId(*page, control_id, control.id))
  792. {
  793. return failure(HmiEditorError::DuplicateId, "同一 HMI 页面内的控件 ID 必须唯一");
  794. }
  795. if (controlsEqual(*existing, control))
  796. {
  797. return {true, HmiEditorError::None, {}, control.id};
  798. }
  799. HistoryState before = captureState();
  800. Project &project = project_service_.editProject();
  801. auto target = std::find_if(
  802. project.hmiPages.begin(), project.hmiPages.end(),
  803. [&page_id](const HmiPage &item) { return item.id == page_id; });
  804. auto editable = std::find_if(
  805. target->controls.begin(), target->controls.end(),
  806. [&control_id](const HmiControl &item) { return item.id == control_id; });
  807. *editable = control;
  808. recordHistory(std::move(before));
  809. return {true, HmiEditorError::None, {}, control.id};
  810. }
  811. // 保留相对布局批量粘贴控件,任一控件失败时整体回滚
  812. HmiEditorResult HmiEditorService::pasteControls(
  813. const std::string &page_id,
  814. const std::vector<HmiControl> &controls,
  815. int offset_x,
  816. int offset_y)
  817. {
  818. const HmiPage *existing_page = findPage(page_id);
  819. if (existing_page == nullptr)
  820. {
  821. return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
  822. }
  823. if (controls.empty())
  824. {
  825. return failure(HmiEditorError::InvalidOperation, "请先复制 HMI 控件");
  826. }
  827. const ProjectLimitSettings &limits = project_service_.projectLimits();
  828. if (controls.size() > limits.maximumHmiControlsPerPage
  829. || existing_page->controls.size() + controls.size()
  830. > limits.maximumHmiControlsPerPage)
  831. {
  832. return failure(
  833. HmiEditorError::InvalidControl,
  834. "当前配置下单个 HMI 页面最多允许 "
  835. + std::to_string(limits.maximumHmiControlsPerPage) + " 个控件");
  836. }
  837. if (totalControlCount(project_service_.project()) + controls.size()
  838. > ProjectLimits::kMaximumHmiControlsPerProject)
  839. {
  840. return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件");
  841. }
  842. // 让整组控件尽量保留相对位置,靠近页面边界时自动向页面内收拢
  843. int minimum_x = controls.front().bounds.x;
  844. int minimum_y = controls.front().bounds.y;
  845. int maximum_x = controls.front().bounds.x + controls.front().bounds.width;
  846. int maximum_y = controls.front().bounds.y + controls.front().bounds.height;
  847. for (const HmiControl &control : controls)
  848. {
  849. minimum_x = std::min(minimum_x, control.bounds.x);
  850. minimum_y = std::min(minimum_y, control.bounds.y);
  851. maximum_x = std::max(
  852. maximum_x, control.bounds.x + control.bounds.width);
  853. maximum_y = std::max(
  854. maximum_y, control.bounds.y + control.bounds.height);
  855. }
  856. if (maximum_x - minimum_x > existing_page->width
  857. || maximum_y - minimum_y > existing_page->height)
  858. {
  859. return failure(HmiEditorError::InvalidControl, "复制的控件尺寸超出目标 HMI 页面");
  860. }
  861. int final_offset_x = offset_x;
  862. int final_offset_y = offset_y;
  863. if (minimum_x + final_offset_x < 0)
  864. {
  865. final_offset_x = -minimum_x;
  866. }
  867. if (minimum_y + final_offset_y < 0)
  868. {
  869. final_offset_y = -minimum_y;
  870. }
  871. if (maximum_x + final_offset_x > existing_page->width)
  872. {
  873. final_offset_x = existing_page->width - maximum_x;
  874. }
  875. if (maximum_y + final_offset_y > existing_page->height)
  876. {
  877. final_offset_y = existing_page->height - maximum_y;
  878. }
  879. const HistoryState before = captureState();
  880. const bool modified_before = project_service_.isModified();
  881. Project &project = project_service_.editProject();
  882. auto target_page = std::find_if(
  883. project.hmiPages.begin(), project.hmiPages.end(),
  884. [&page_id](const HmiPage &candidate) { return candidate.id == page_id; });
  885. std::vector<std::string> new_ids;
  886. new_ids.reserve(controls.size());
  887. for (const HmiControl &source : controls)
  888. {
  889. const HmiControlDescriptor *descriptor = findHmiControlDescriptor(source.type);
  890. if (descriptor == nullptr)
  891. {
  892. project.hmiPages = before.pages;
  893. project.initialHmiPageId = before.initial_page_id;
  894. project_service_.restoreModifiedState(modified_before);
  895. return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型");
  896. }
  897. HmiControl candidate = source;
  898. candidate.id = makeUniqueId(*target_page, descriptor->idPrefix);
  899. candidate.bounds.x += final_offset_x;
  900. candidate.bounds.y += final_offset_y;
  901. std::string error;
  902. if (!validateEditableControl(*target_page, candidate, &error))
  903. {
  904. project.hmiPages = before.pages;
  905. project.initialHmiPageId = before.initial_page_id;
  906. project_service_.restoreModifiedState(modified_before);
  907. return failure(HmiEditorError::InvalidControl, error);
  908. }
  909. new_ids.push_back(candidate.id);
  910. target_page->controls.push_back(std::move(candidate));
  911. }
  912. recordHistory(before);
  913. return {true, HmiEditorError::None, {}, new_ids.front()};
  914. }
  915. // 判断当前 HMI 编辑会话是否有可撤销操作
  916. bool HmiEditorService::canUndo() const
  917. {
  918. return history_.canUndo();
  919. }
  920. // 判断当前 HMI 编辑会话是否有可重做操作
  921. bool HmiEditorService::canRedo() const
  922. {
  923. return history_.canRedo();
  924. }
  925. // 恢复最近一份编辑前快照
  926. HmiEditorResult HmiEditorService::undo()
  927. {
  928. const std::optional<HistoryState> target = history_.undo(captureState());
  929. if (!target.has_value())
  930. {
  931. return historyFailure("没有可撤销的 HMI 编辑操作");
  932. }
  933. Project &project = project_service_.editProject();
  934. project.hmiPages = target->pages;
  935. project.initialHmiPageId = target->initial_page_id;
  936. return {true, HmiEditorError::None, {}, {}};
  937. }
  938. // 重新应用最近一份被撤销的快照
  939. HmiEditorResult HmiEditorService::redo()
  940. {
  941. const std::optional<HistoryState> target = history_.redo(captureState());
  942. if (!target.has_value())
  943. {
  944. return historyFailure("没有可重做的 HMI 编辑操作");
  945. }
  946. Project &project = project_service_.editProject();
  947. project.hmiPages = target->pages;
  948. project.initialHmiPageId = target->initial_page_id;
  949. return {true, HmiEditorError::None, {}, {}};
  950. }
  951. // 清空当前 HMI 编辑会话的撤销和重做历史
  952. void HmiEditorService::clearHistory()
  953. {
  954. history_.clear();
  955. }
  956. // 校验控件自身配置、页面边界和页面跳转目标
  957. bool HmiEditorService::validateEditableControl(
  958. const HmiPage &page, const HmiControl &control, std::string *error) const
  959. {
  960. if (!control.validate(error))
  961. {
  962. return false;
  963. }
  964. if (control.bounds.x < 0 || control.bounds.y < 0
  965. || control.bounds.width > page.width || control.bounds.height > page.height
  966. || control.bounds.x > page.width - control.bounds.width
  967. || control.bounds.y > page.height - control.bounds.height)
  968. {
  969. setError(error, "HMI 控件不能超出页面范围");
  970. return false;
  971. }
  972. if (control.type == HmiControlType::PageJump
  973. && !control.pageJump->targetPageId.empty()
  974. && findPage(control.pageJump->targetPageId) == nullptr)
  975. {
  976. setError(error, "页面跳转控件的目标页面不存在");
  977. return false;
  978. }
  979. return true;
  980. }
  981. // 检查候选控件 ID 是否与页面内其他控件重复
  982. bool HmiEditorService::hasDuplicateControlId(
  983. const HmiPage &page,
  984. const std::string &excluded_id,
  985. const std::string &candidate_id)
  986. {
  987. return std::any_of(
  988. page.controls.cbegin(), page.controls.cend(),
  989. [&excluded_id, &candidate_id](const HmiControl &item)
  990. {
  991. // 排除正在编辑的原控件,只检查同页的其他控件
  992. return item.id != excluded_id && item.id == candidate_id;
  993. });
  994. }
  995. // 根据控件类型生成默认样式、文字、大小、初始坐标
  996. HmiControl HmiEditorService::makeControl(
  997. const HmiPage &page, const HmiControlDescriptor &descriptor)
  998. {
  999. HmiControl control;
  1000. control.id = makeUniqueId(page, descriptor.idPrefix);
  1001. control.type = descriptor.type;
  1002. control.text = descriptor.defaultText;
  1003. control.bounds.width = std::min(descriptor.defaultBounds.width, page.width);
  1004. control.bounds.height = std::min(descriptor.defaultBounds.height, page.height);
  1005. if (descriptor.type == HmiControlType::PageJump)
  1006. {
  1007. control.pageJump = HmiPageJumpConfig{};
  1008. }
  1009. if (descriptor.type == HmiControlType::StatusText)
  1010. {
  1011. control.statusText = HmiStatusBitTextConfig{
  1012. "设备停止", "设备运行"};
  1013. }
  1014. const int offset = static_cast<int>(page.controls.size()) * 16;
  1015. control.bounds.x = std::min(20 + offset, page.width - control.bounds.width);
  1016. control.bounds.y = std::min(20 + offset, page.height - control.bounds.height);
  1017. return control;
  1018. }
  1019. // 生成由类型前缀和递增数字组成的页面内唯一控件 ID
  1020. std::string HmiEditorService::makeUniqueId(
  1021. const HmiPage &page, const std::string &prefix)
  1022. {
  1023. // 同类控件从 1 递增命名,保证页面内标识稳定且唯一
  1024. int suffix = 1;
  1025. while (true)
  1026. {
  1027. const std::string candidate = prefix + '-' + std::to_string(suffix);
  1028. const bool found = std::any_of(
  1029. page.controls.cbegin(), page.controls.cend(),
  1030. [&candidate](const HmiControl &item) { return item.id == candidate; });
  1031. if (!found)
  1032. {
  1033. return candidate;
  1034. }
  1035. ++suffix;
  1036. }
  1037. }