diff --git a/TeXmacs/misc/images/tutorial/stem-image.png b/TeXmacs/misc/images/tutorial/stem-image.png new file mode 100644 index 0000000000..d7dbc7c0fe Binary files /dev/null and b/TeXmacs/misc/images/tutorial/stem-image.png differ diff --git a/TeXmacs/plugins/tutorial/data/first-launch-tutorial.json b/TeXmacs/plugins/tutorial/data/first-launch-tutorial.json index 3ad2179485..e19eaf1dd7 100644 --- a/TeXmacs/plugins/tutorial/data/first-launch-tutorial.json +++ b/TeXmacs/plugins/tutorial/data/first-launch-tutorial.json @@ -10,7 +10,7 @@ "offset-x": 0, "offset-y": 0, "media-path": "", - "bottom-text": "这是 Liii STEM 的主工作区。教程会依次介绍几个核心功能,帮助你快速上手。", + "bottom-text": "这是 Liii STEM 的主工作区。教程会依次介绍几个核心功能,帮助您快速上手。", "target-id": "", "placement": "bottom", "highlight-padding": 0, @@ -20,7 +20,7 @@ { "id": "login", "title": "登录账户", - "top-text": "点击这里登录你的账户,解锁更多高级功能和扩展能力。", + "top-text": "点击这里登录您的账户,解锁更多高级功能和扩展能力。", "bubble-size": "medium", "offset-x": -20, "offset-y": 0, @@ -35,31 +35,33 @@ { "id": "magic-paste", "title": "魔法粘贴", - "top-text": "在编辑区使用 Ctrl+V 粘贴内容时,粘贴板会根据内容类型自动选择合适的格式,让你的排版更加高效。", + "top-text": "魔法粘贴可以自动识别剪贴板中的内容类型,并尽可能保留网页内容的结构、标题层级、列表样式和公式表达,让外部内容进入文档后依然便于继续编辑与排版。", "bubble-size": "large", "offset-x": 500, "offset-y": 0, - "media-path": "../../../misc/images/tutorial/test.gif", - "bottom-text": "", + "media-path": ":/tutorial/test.gif", + "bottom-text": "系统已经为您打开了一个新的空白文档,并将一段网页内容复制到了剪贴板。请在编辑区中使用 Ctrl+Shift+V(Windows / Linux)或 Command+Shift+V(macOS)进行粘贴,体验魔法粘贴对网页内容结构的自动识别与导入。完成本步操作后,才可以进入下一步。", "target-id": "editorArea", "placement": "top", "highlight-padding": 12, - "on-enter": "", + "require-action": "magic-paste", + "on-enter": "(tutorial-prepare-magic-paste-demo)", "skip-if-missing": true }, { "id": "ocr", "title": "OCR 文字识别", - "top-text": "如果你的剪贴板中包含图片或PDF内容,编辑区会自动提示是否需要提取其中的文字,让你快速将扫描文档或截图转换为可编辑文本。", + "top-text": "如果您的剪贴板中包含图片内容,软件可以通过快捷键触发 OCR ,提取其中的文字,让您快速将截图转换为可编辑文本;您也可以在插入图片后,通过图片悬浮菜单上的 OCR 按钮发起识别。", "bubble-size": "large", "offset-x": 500, "offset-y": 0, - "media-path": "../../../misc/images/tutorial/test.gif", - "bottom-text": "", + "media-path": ":/tutorial/test.gif", + "bottom-text": "系统已经为您打开了一个包含图片的文档,且将一张图片放入了您的剪贴板,您可以在编辑区中使用 Ctrl+Alt+V(Windows / Linux)或 Command+Option+V(macOS)触发 OCR 识别,也可以用鼠标点击悬浮菜单中的按钮触发 OCR 识别,图片中的文字将被快速提取为文档中的可编辑文本。完成本步操作后,才可以进入下一步。", "target-id": "editorArea", "placement": "top", "highlight-padding": 12, - "on-enter": "", + "require-action": "ocr-paste", + "on-enter": "(tutorial-prepare-ocr-demo)", "skip-if-missing": true } ] diff --git a/TeXmacs/plugins/tutorial/data/ocr-demo.tmu b/TeXmacs/plugins/tutorial/data/ocr-demo.tmu new file mode 100644 index 0000000000..c404e53039 --- /dev/null +++ b/TeXmacs/plugins/tutorial/data/ocr-demo.tmu @@ -0,0 +1,14 @@ +> + +> + +<\body> + |png>|0.8par|0.693123par||>> + + +<\initial> + <\collection> + + + + diff --git a/TeXmacs/plugins/tutorial/data/zhihu-magic-paste-demo.html b/TeXmacs/plugins/tutorial/data/zhihu-magic-paste-demo.html new file mode 100644 index 0000000000..fdd41585b5 --- /dev/null +++ b/TeXmacs/plugins/tutorial/data/zhihu-magic-paste-demo.html @@ -0,0 +1,29 @@ + + + + + 知乎回答示例 - 魔法粘贴体验 + + +
+

为什么数学公式在排版软件里经常显得难编辑?

+

因为大多数编辑器只擅长处理纯文本,而不擅长处理结构化内容。

+

当一段内容同时包含标题、正文、列表、强调和公式时,编辑器需要理解它的语义结构,而不只是把它当成一串字符。

+
+

真正高效的编辑体验,不是“把格式补回来”,而是“粘贴进来时就尽量保留结构”。

+
+

一个简单例子

+

如果原文里有如下结论:

+

对于任意实数 ab,都有

+

$$ (a+b)^2 = a^2 + 2ab + b^2 $$

+

那么理想的粘贴结果应该尽量保留:

+
    +
  • 标题层级
  • +
  • 段落结构
  • +
  • 强调样式
  • +
  • 公式表达
  • +
+

这也是“魔法粘贴”存在的意义:让来自网页、问答平台和 AI 对话的内容,进入文档后仍然尽量可编辑、可继续排版。

+
+ + diff --git a/TeXmacs/plugins/tutorial/progs/init-tutorial.scm b/TeXmacs/plugins/tutorial/progs/init-tutorial.scm index fb8c61fc47..169d8eee09 100644 --- a/TeXmacs/plugins/tutorial/progs/init-tutorial.scm +++ b/TeXmacs/plugins/tutorial/progs/init-tutorial.scm @@ -10,5 +10,36 @@ ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(tm-define (tutorial-open-something) - (display* "tutorial enter editor step\n")) +(define (tutorial-magic-paste-demo-path) + (unix->url "$TEXMACS_PATH/plugins/tutorial/data/zhihu-magic-paste-demo.html")) + +(define tutorial-magic-paste-demo-opened? #f) +(define tutorial-ocr-demo-opened? #f) + +(define (tutorial-ocr-demo-document-path) + (unix->url "$TEXMACS_PATH/plugins/tutorial/data/ocr-demo.tmu")) + +(define (tutorial-ocr-demo-image-path) + (unix->url "$TEXMACS_PATH/misc/images/tutorial/stem-image.png")) + +(tm-define (tutorial-notify-action action) + (cpp-set-preference "tutorial:last-action" action)) + +(tm-define (tutorial-prepare-magic-paste-demo) + (let* ((html-path (tutorial-magic-paste-demo-path)) + (html (utf8->cork (string-load html-path))) + (old-export (clipboard-get-export))) + (if (not tutorial-magic-paste-demo-opened?) + (begin + (new-document) + (set! tutorial-magic-paste-demo-opened? #t))) + (clipboard-set-export "verbatim") + (clipboard-set "primary" html) + (clipboard-set-export old-export))) + +(tm-define (tutorial-prepare-ocr-demo) + (if (not tutorial-ocr-demo-opened?) + (begin + (load-document (tutorial-ocr-demo-document-path)) + (set! tutorial-ocr-demo-opened? #t))) + (graphics-file-to-clipboard (tutorial-ocr-demo-image-path))) diff --git a/TeXmacs/progs/generic/generic-edit.scm b/TeXmacs/progs/generic/generic-edit.scm index f69e961453..e3d0153d90 100644 --- a/TeXmacs/progs/generic/generic-edit.scm +++ b/TeXmacs/progs/generic/generic-edit.scm @@ -521,7 +521,9 @@ ocr-paste (with data (parse-texmacs-snippet (tree->string (tree-ref (clipboard-get "primary") 1))) (when (tree-is? (tree-ref data 0) 'image) - (ocr-to-latex-by-cursor data)))) + (ocr-to-latex-by-cursor data) + (when (defined? 'tutorial-notify-action) + (tutorial-notify-action "ocr-paste"))))) #| image-and-ocr-paste @@ -539,7 +541,9 @@ image-and-ocr-paste (kbd-return) (when (not (defined? 'ocr-to-latex-by-cursor)) (use-modules (liii ocr))) - (ocr-to-latex-by-cursor data)))) + (ocr-to-latex-by-cursor data) + (when (defined? 'tutorial-notify-action) + (tutorial-notify-action "ocr-paste"))))) (tm-define (paste-as-html) (with source-format (qt-clipboard-format) @@ -626,7 +630,10 @@ TODO: 在文本模式中,可以自动识别剪贴板中的内容,并智能 (clipboard-paste-import "code" "primary")) ((== mode "math") (clipboard-paste-import "latex" "primary")) - (else (smart-format-paste)))))) + (else (smart-format-paste))))) + (when (and (defined? 'tutorial-notify-action) + (not (string-starts? (qt-clipboard-format) "image"))) + (tutorial-notify-action "magic-paste"))) (tm-define (any-image-context?) (tree-innermost diff --git a/devel/222_68.md b/devel/222_68.md index bdbacf3c9a..6bb63eb58c 100644 --- a/devel/222_68.md +++ b/devel/222_68.md @@ -24,6 +24,7 @@ + `media-path`:正文中间图片或 GIF 路径,可为空 + `bottom-text`:正文下方文字,可为空 + `on-enter`:进入该步骤时执行一次的 Scheme 命令,可为空 ++ `require-action`:要求用户先完成某个动作后才能进入下一步,可为空 + `target-id`:步骤要高亮的目标区域,可为空;为空时仅显示说明卡片,不绘制高亮区域 + `placement`:说明卡片相对高亮区域的位置,可选值为 `auto / top / bottom / left / right` + `bubble-size`:说明卡片尺寸,可选值为 `small / medium / large`,未设置时默认为 `medium` @@ -37,6 +38,7 @@ + 调整步骤顺序:直接修改 `steps` 数组中的对象顺序 + 修改步骤文案:修改 `title`、`top-text`、`bottom-text` + 进入时执行动作:修改 `on-enter` ++ 控制下一步解锁条件:修改 `require-action` + 修改高亮目标:修改 `target-id` + 修改卡片位置:修改 `placement` + 修改卡片尺寸:修改 `bubble-size` @@ -68,6 +70,7 @@ + 步骤支持配置卡片偏移量 `offset-x` / `offset-y` + 首次启动教程第三步、第四步接入了 GIF 演示图 + 第二步卡片位置从 `left` 调整为 `bottom`,避免默认布局下更容易贴边 ++ 第三步、第四步支持“完成操作后才允许下一步”的约束 ### Why @@ -77,13 +80,14 @@ + 欢迎页实际上不需要高亮任何区域,却被迫绑定一个大范围目标 + 某些步骤需要更大的卡片去承载 GIF,而有些步骤只需要较小卡片 + 仅依赖 `placement` 的四向布局还不够,部分步骤需要做像素级微调 ++ 第三步和第四步属于操作体验步骤,如果用户没有真正完成粘贴或 OCR,就不应允许直接跳到后续步骤 这些问题都不应该通过继续硬编码 UI 分支来解决,更合适的方式是把能力下沉到教程配置模型中。 ### How -+ `TutorialStepConfig` 中新增 `bubbleSize`、`offsetX`、`offsetY` 字段,默认值分别为 `medium`、`0`、`0` -+ `TutorialConfigLoader::loadFlow()` 读取步骤时,额外解析 `bubble-size`、`offset-x`、`offset-y` ++ `TutorialStepConfig` 中新增 `bubbleSize`、`offsetX`、`offsetY`、`requiredAction` 字段,默认值分别为 `medium`、`0`、`0`、空字符串 ++ `TutorialConfigLoader::loadFlow()` 读取步骤时,额外解析 `bubble-size`、`offset-x`、`offset-y`、`require-action` + `bubble-size` 当前支持三档:`small / medium / large` + `TutorialBubble::setStep()` 根据 `bubble-size` 切换卡片固定宽度和媒体区域尺寸: `small` 对应 `300` 宽和 `240x144` 媒体区 @@ -92,11 +96,17 @@ + `TutorialBubble::setStep()` 在 `media-path` 为空时显式隐藏 `m_mediaLabel`,避免空白占位 + `TutorialEngine::showStep()` 对 `target-id` 为空的步骤直接走“仅展示卡片、清空高亮”的分支 + `TutorialOverlay::bubbleRectForPlacement()` 在最终气泡坐标上叠加 `offset-x` / `offset-y` ++ `TutorialEngine` 为声明了 `require-action` 的步骤禁用“下一步”按钮,并轮询临时偏好 `tutorial:last-action`;动作完成后再解锁 ++ `TeXmacs/plugins/tutorial/progs/init-tutorial.scm` 中新增 `tutorial-notify-action` ++ `TeXmacs/progs/generic/generic-edit.scm` 中的 `kbd-magic-paste`、`ocr-paste`、`image-and-ocr-paste` 在实际执行后会上报 tutorial action ++ `src/Plugins/Qt/QTMImagePopup.cpp` 中图片悬浮菜单的 OCR 按钮也会上报 `ocr-paste`,因此第四步既支持快捷键,也支持悬浮按钮解锁 + `TeXmacs/plugins/tutorial/data/first-launch-tutorial.json` 当前已使用这些能力: 第一页 `target-id` 置空,用作无高亮欢迎页 第二页 `placement` 调整为 `bottom` 第三页和第四页的 `bubble-size` 调整为 `large` - 第三页和第四页接入 `TeXmacs/misc/images/tutorial/test.gif` + 第三页和第四页接入 Qt 资源路径 `:/tutorial/test.gif` + 第三页设置 `require-action = "magic-paste"` + 第四页设置 `require-action = "ocr-paste"` ## 2026/04/14 首次启动教程当前配置 @@ -119,7 +129,36 @@ + `placement + bubble-size + offset-x + offset-y` + `target-id=""` 的无高亮欢迎页 -+ `media-path` 指向相对路径 GIF 的演示步骤 ++ `media-path` 指向 Qt 资源路径 GIF 的演示步骤 ++ `on-enter + require-action` 的操作型步骤 + +## 2026/04/14 首次启动教程操作型步骤 + +### What + +当前第三步与第四步不再只是“展示说明”,而是具备明确的“先操作,后放行”行为: + ++ 第三步进入时打开新的无标题文档,并准备一份 mock HTML 到剪贴板 ++ 第四步进入时打开 OCR 示例文档,并准备一张示例图片到剪贴板 ++ 两步都只有在用户实际完成目标操作后,才允许点击“下一步” + +### How + ++ 第三步 `on-enter` 调用 `(tutorial-prepare-magic-paste-demo)`: + 第一次进入时 `new-document` + 读取 `TeXmacs/plugins/tutorial/data/zhihu-magic-paste-demo.html` + 经 `utf8->cork` 后放入剪贴板 ++ 第四步 `on-enter` 调用 `(tutorial-prepare-ocr-demo)`: + 第一次进入时加载 `TeXmacs/plugins/tutorial/data/ocr-demo.tmu` + 将 `TeXmacs/misc/images/tutorial/stem-image.png` 放入剪贴板 ++ 第三步当前认可的完成入口: + `Ctrl+Shift+V`(Windows / Linux) + `Command+Shift+V`(macOS) ++ 第四步当前认可的完成入口: + `Ctrl+Alt+V`(Windows / Linux) + `Command+Option+V`(macOS) + 图片悬浮菜单上的 OCR 按钮 ++ 第三步和第四步的文案中已经显式提示“完成本步操作后,才可以进入下一步” ## 2026/04/03 聚光灯教程基础设施 diff --git a/src/Plugins/Qt/QTMImagePopup.cpp b/src/Plugins/Qt/QTMImagePopup.cpp index bc7d256979..ee6355ad83 100644 --- a/src/Plugins/Qt/QTMImagePopup.cpp +++ b/src/Plugins/Qt/QTMImagePopup.cpp @@ -69,8 +69,11 @@ QTMImagePopup::QTMImagePopup (QWidget* parent, qt_simple_widget_rep* owner) call ("set-image-alignment", current_tree, "center"); else if (button == rightBtn) call ("set-image-alignment", current_tree, "right"); - else if (button == ocrBtn) + else if (button == ocrBtn) { call ("ocr-to-latex-by-image", current_tree); + eval ("(when (defined? 'tutorial-notify-action) " + "(tutorial-notify-action \"ocr-paste\"))"); + } current_align= as_string (call ("get-image-alignment", current_tree)); }); diff --git a/src/Plugins/Qt/qt_tutorial.cpp b/src/Plugins/Qt/qt_tutorial.cpp index c6c2da3c72..f6b649f8cc 100644 --- a/src/Plugins/Qt/qt_tutorial.cpp +++ b/src/Plugins/Qt/qt_tutorial.cpp @@ -187,6 +187,13 @@ parseStepEntry (const json& stepJson, QWK::TutorialStepConfig& step, QString ("Tutorial step %1 on-enter must be a string").arg (step.id); return false; } + if (!readStringField ("require-action", step.requiredAction)) { + if (errorMessage != nullptr) + *errorMessage= + QString ("Tutorial step %1 require-action must be a string") + .arg (step.id); + return false; + } if (!hasTopTextField && step.mediaPath.isEmpty () && step.bottomText.isEmpty ()) { @@ -305,6 +312,8 @@ firstLaunchTutorialConfigPath () { "$TEXMACS_PATH/plugins/tutorial/data/first-launch-tutorial.json"); } +constexpr const char* kTutorialLastActionPreference= "tutorial:last-action"; + } // namespace namespace QWK { @@ -490,17 +499,17 @@ TutorialBubble::TutorialBubble (QWidget* parent) } QLabel#tutorialTitle { color: #122033; - font-size: 17px; + font-size: 20px; font-weight: 700; } QLabel#tutorialBodyText { color: #334155; - font-size: 13px; + font-size: 16px; line-height: 1.5; } QLabel#tutorialProgress { color: #6b7280; - font-size: 12px; + font-size: 13px; font-weight: 600; } QPushButton { @@ -520,7 +529,9 @@ TutorialBubble::TutorialBubble (QWidget* parent) "#d1d5db; }")); m_nextButton->setStyleSheet (QStringLiteral ( "QPushButton { background: #0f766e; color: white; border: 1px solid " - "#0f766e; }")); + "#0f766e; } " + "QPushButton:disabled { background: #cbd5e1; color: #64748b; border: " + "1px solid #cbd5e1; }")); connect (m_previousButton, &QPushButton::clicked, this, &TutorialBubble::previousRequested); @@ -635,6 +646,12 @@ TutorialBubble::setLastStep (bool last) { : qt_translate ("下一步")); } +void +TutorialBubble::setNextEnabled (bool enabled, const QString& toolTip) { + m_nextButton->setEnabled (enabled); + m_nextButton->setToolTip (toolTip); +} + TutorialOverlay::TutorialOverlay (QMainWindow* parentWindow) : QWidget (parentWindow), m_parentWindow (parentWindow), m_bubble (new TutorialBubble (this)), m_hasHighlight (false) { @@ -798,6 +815,11 @@ TutorialOverlay::repositionBubble (TutorialPlacement placement) { m_bubble->raise (); } +void +TutorialOverlay::setNextEnabled (bool enabled, const QString& toolTip) { + m_bubble->setNextEnabled (enabled, toolTip); +} + void TutorialOverlay::paintEvent (QPaintEvent* event) { (void) event; @@ -839,7 +861,11 @@ TutorialOverlay::wheelEvent (QWheelEvent* event) { TutorialEngine::TutorialEngine (QObject* parent) : QObject (parent), m_currentIndex (-1), m_displayedIndex (-1), - m_stepRequestId (0) {} + m_stepRequestId (0), m_actionPollTimer (new QTimer (this)) { + m_actionPollTimer->setInterval (150); + connect (m_actionPollTimer, &QTimer::timeout, this, + &TutorialEngine::pollRequiredAction); +} bool TutorialEngine::start (QMainWindow* hostWindow, @@ -855,7 +881,9 @@ TutorialEngine::start (QMainWindow* hostWindow, m_currentIndex = -1; m_displayedIndex= -1; m_stepRequestId = 0; - m_overlay = new TutorialOverlay (hostWindow); + m_completedActionSteps.clear (); + reset_user_preference (kTutorialLastActionPreference); + m_overlay= new TutorialOverlay (hostWindow); m_overlay->show (); m_overlay->raise (); @@ -875,6 +903,8 @@ TutorialEngine::start (QMainWindow* hostWindow, void TutorialEngine::stop (TutorialFinishReason reason) { if (m_hostWindow != nullptr) m_hostWindow->removeEventFilter (this); + m_actionPollTimer->stop (); + reset_user_preference (kTutorialLastActionPreference); if (m_overlay != nullptr) { m_overlay->hide (); @@ -886,8 +916,9 @@ TutorialEngine::stop (TutorialFinishReason reason) { m_currentIndex = -1; m_displayedIndex= -1; m_stepRequestId = 0; - m_config = TutorialFlowConfig (); - m_registry = TutorialTargetRegistry (); + m_completedActionSteps.clear (); + m_config = TutorialFlowConfig (); + m_registry= TutorialTargetRegistry (); emit finished (reason); } @@ -942,6 +973,51 @@ TutorialEngine::executeOnEnter (const TutorialStepConfig& step) { exec_delayed (scheme_cmd (from_qstring (step.onEnterCommand))); } +void +TutorialEngine::updateCurrentStepGate () { + if (!isActive () || m_overlay == nullptr || m_currentIndex < 0 || + m_currentIndex >= m_config.steps.size ()) { + if (m_actionPollTimer->isActive ()) m_actionPollTimer->stop (); + return; + } + + const TutorialStepConfig& step= m_config.steps[m_currentIndex]; + if (step.requiredAction.trimmed ().isEmpty () || + m_completedActionSteps.contains (step.id)) { + m_overlay->setNextEnabled (true); + if (m_actionPollTimer->isActive ()) m_actionPollTimer->stop (); + return; + } + + reset_user_preference (kTutorialLastActionPreference); + m_overlay->setNextEnabled ( + false, qt_translate ("完成当前步骤要求的粘贴操作后才可继续")); + if (!m_actionPollTimer->isActive ()) m_actionPollTimer->start (); +} + +void +TutorialEngine::pollRequiredAction () { + if (!isActive () || m_currentIndex < 0 || + m_currentIndex >= m_config.steps.size ()) { + m_actionPollTimer->stop (); + return; + } + + const TutorialStepConfig& step= m_config.steps[m_currentIndex]; + if (step.requiredAction.trimmed ().isEmpty ()) { + m_actionPollTimer->stop (); + return; + } + + const QString lastAction= + to_qstring (get_user_preference (kTutorialLastActionPreference, "")); + if (lastAction != step.requiredAction) return; + + m_completedActionSteps.insert (step.id); + reset_user_preference (kTutorialLastActionPreference); + updateCurrentStepGate (); +} + void TutorialEngine::updateOverlayGeometry () { if (m_overlay == nullptr || m_hostWindow == nullptr) return; @@ -990,6 +1066,7 @@ TutorialEngine::showStep (int index, int retryCount, int fallbackDirection, const TutorialStepConfig& step= m_config.steps[index]; if (step.targetId.trimmed ().isEmpty ()) { m_overlay->setStep (step, index, m_config.steps.size ()); + updateCurrentStepGate (); m_overlay->clearHighlight (); m_overlay->show (); m_overlay->raise (); @@ -1017,6 +1094,7 @@ TutorialEngine::showStep (int index, int retryCount, int fallbackDirection, } m_overlay->setStep (step, index, m_config.steps.size ()); + updateCurrentStepGate (); m_overlay->clearHighlight (); m_overlay->show (); m_overlay->raise (); @@ -1029,6 +1107,7 @@ TutorialEngine::showStep (int index, int retryCount, int fallbackDirection, } m_overlay->setStep (step, index, m_config.steps.size ()); + updateCurrentStepGate (); m_overlay->setHighlightedRect (rect, step.highlightPadding); m_overlay->show (); m_overlay->raise (); diff --git a/src/Plugins/Qt/qt_tutorial.hpp b/src/Plugins/Qt/qt_tutorial.hpp index 6658db4dd9..a67ea5df82 100644 --- a/src/Plugins/Qt/qt_tutorial.hpp +++ b/src/Plugins/Qt/qt_tutorial.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ class QPushButton; class QEvent; class QMouseEvent; class QPaintEvent; +class QTimer; class QWheelEvent; namespace QWK { @@ -40,6 +42,7 @@ struct TutorialStepConfig { QString mediaPath; QString bottomText; QString onEnterCommand; + QString requiredAction; TutorialBubbleSize bubbleSize= TutorialBubbleSize::Medium; int offsetX = 0; int offsetY = 0; @@ -81,6 +84,7 @@ class TutorialBubble : public QWidget { void setStep (const TutorialStepConfig& step, int index, int total); void setFirstStep (bool first); void setLastStep (bool last); + void setNextEnabled (bool enabled, const QString& toolTip= QString ()); signals: void previousRequested (); @@ -110,6 +114,7 @@ class TutorialOverlay : public QWidget { void setHighlightedRect (const QRect& rect, int padding); void clearHighlight (); void repositionBubble (TutorialPlacement placement); + void setNextEnabled (bool enabled, const QString& toolTip= QString ()); signals: void previousRequested (); @@ -160,6 +165,8 @@ class TutorialEngine : public QObject { private: void executeOnEnter (const TutorialStepConfig& step); + void updateCurrentStepGate (); + void pollRequiredAction (); void refreshCurrentStepGeometry (); void updateOverlayGeometry (); void showStep (int index, int retryCount= 0, int fallbackDirection= 0, @@ -175,6 +182,8 @@ class TutorialEngine : public QObject { int m_currentIndex; int m_displayedIndex; int m_stepRequestId; + QSet m_completedActionSteps; + QTimer* m_actionPollTimer; }; class FirstLaunchTutorialController : public QObject {