Skip to content

fix(vnote): flush editor content before note save actions#377

Draft
dengzhongyuan365-dev wants to merge 1 commit into
linuxdeepin:develop/snipefrom
dengzhongyuan365-dev:bug-fix-6-17
Draft

fix(vnote): flush editor content before note save actions#377
dengzhongyuan365-dev wants to merge 1 commit into
linuxdeepin:develop/snipefrom
dengzhongyuan365-dev:bug-fix-6-17

Conversation

@dengzhongyuan365-dev

@dengzhongyuan365-dev dengzhongyuan365-dev commented Jun 22, 2026

Copy link
Copy Markdown
Member

Bind editor flush requests to note id and change serial.

将编辑器 flush 请求绑定到笔记 ID 和变更序号。

Reuse flushed note content before context menu and save actions.

右键菜单和保存动作前复用已同步的笔记内容。

Log: 修复新建笔记快速右键保存菜单置灰问题
PMS: BUG-366545
Influence: 新建或编辑笔记后立即右键、快捷键弹出菜单或保存时,会先同步编辑器最新内容,避免保存笔记菜单误置灰或导出旧内容。

Summary by Sourcery

Bind editor flush requests and note update operations to a specific note ID and text change serial to avoid stale saves and ensure latest content is used for actions like context menus and save.

Bug Fixes:

  • Prevent saving or exporting outdated content by discarding stale note update results that do not match the current note ID and text change serial.
  • Fix note context menu save option being incorrectly disabled for newly created or recently edited notes by flushing and persisting editor content before opening menus or saving.

Enhancements:

  • Add support for flushing the current note content on demand and reusing the flushed result for context menus and keyboard save actions.
  • Introduce a serial-based mechanism in the rich text manager and main manager to track text changes and validate note update results.
  • Refactor note context menu invocation in the item list and main window to centralize pre-checks and ensure editor content is synchronized before menu display or save operations.

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: dengzhongyuan365-dev

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR ensures the rich text editor flushes and binds its content updates to a specific note ID and change serial before context menu and save actions, preventing stale or missing content when quickly saving or opening menus.

Sequence diagram for serial-bound note update from editor

sequenceDiagram
    actor User
    participant WebRichTextManager
    participant VNoteMainManager
    participant WebEngineView
    participant webView

    User->>WebRichTextManager: updateNote()
    WebRichTextManager->>VNoteMainManager: needUpdateNote(noteId, serial)
    VNoteMainManager-->>WebEngineView: needUpdateNote(noteId, serial)
    WebEngineView->>webView: runJavaScript(getHtml)
    webView-->>WebEngineView: result
    WebEngineView->>VNoteMainManager: updateNoteWithResultForNote(noteId, serial, result)
    VNoteMainManager->>WebRichTextManager: onUpdateNoteWithResult(note, serial, result)
    WebRichTextManager->>WebRichTextManager: saveNoteWithResult(note, result)
    WebRichTextManager-->>VNoteMainManager: finishedUpdateNote()
Loading

Sequence diagram for flushing editor before note context menu and save

sequenceDiagram
    actor User
    participant ItemListView
    participant MainWindow
    participant WebEngineView
    participant webView
    participant VNoteMainManager
    participant WebRichTextManager

    User->>ItemListView: requestNoteContextMenu(...)
    ItemListView-->>MainWindow: requestNoteContextMenu(noteIds, popupIndex, popupX, popupY)
    MainWindow->>WebEngineView: flushCurrentNote(callback)
    WebEngineView->>VNoteMainManager: currentNoteId()
    WebEngineView->>VNoteMainManager: currentTextChangeSerial()
    WebEngineView->>webView: runJavaScript(getHtml)
    webView-->>WebEngineView: result
    WebEngineView->>VNoteMainManager: flushNoteWithResultForNote(noteId, serial, result)
    VNoteMainManager->>WebRichTextManager: flushNoteWithResult(note, serial, result)
    WebRichTextManager->>WebRichTextManager: saveNoteWithResult(note, result)
    WebRichTextManager->>WebRichTextManager: clearTextChangeState(noteId, serial)
    WebRichTextManager-->>VNoteMainManager: finishedUpdateNote()
    WebEngineView-->>MainWindow: callback()
    MainWindow-->>ItemListView: popupNoteContextMenu(noteIds, popupIndex, popupX, popupY)
Loading

File-Level Changes

Change Details Files
Bind note update and flush operations to note ID and text change serial to avoid applying stale editor results.
  • Extend needUpdateNote signal and handler chain to carry both noteId and serial from WebRichTextManager through VNoteMainManager to WebEngineView and back.
  • Refactor onUpdateNoteWithResult to validate noteId/serial against both the requested update and current text change state, discarding stale or outdated results.
  • Introduce helpers saveNoteWithResult and clearTextChangeState to centralize note saving and state reset, including a new flushNoteWithResult path that emits finishedUpdateNote only when the corresponding request is cleared.
src/common/webrichetextmanager.cpp
src/common/webrichetextmanager.h
src/common/VNoteMainManager.cpp
src/common/VNoteMainManager.h
src/gui/mainwindow/WebEngineView.qml
Ensure note content is flushed from the web editor before triggering context menu and save operations.
  • Add WebEngineView.flushCurrentNote, which captures current noteId and text change serial, fetches HTML via runJavaScript, and calls flushNoteWithResultForNote.
  • Route ItemListView context-menu triggers and keyboard save actions through flushCurrentNote callbacks before checking hasNoteText or popping menus, reusing the flushed content.
  • Introduce a requestNoteContextMenu signal and popupNoteContextMenu helper to centralize note context-menu popping after content has been flushed.
src/gui/mainwindow/WebEngineView.qml
src/gui/mainwindow/MainWindow.qml
src/gui/mainwindow/ItemListView.qml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

Copy link
Copy Markdown
  • 敏感词检查失败, 检测到1个文件存在敏感词
详情
{
    "src/common/VNoteMainManager.cpp": [
        {
            "line": "            url = \"https://www.deepin.org/zh/agreement/privacy/\";",
            "line_number": 1374,
            "rule": "S35",
            "reason": "Url link | 20e2eab189"
        },
        {
            "line": "            url = \"https://www.uniontech.com/agreement/privacy-cn\";",
            "line_number": 1376,
            "rule": "S35",
            "reason": "Url link | 4850a00dd7"
        },
        {
            "line": "            url = \"https://www.deepin.org/en/agreement/privacy/\";",
            "line_number": 1380,
            "rule": "S35",
            "reason": "Url link | 38d42f63bf"
        },
        {
            "line": "            url = \"https://www.uniontech.com/agreement/privacy-en\";",
            "line_number": 1382,
            "rule": "S35",
            "reason": "Url link | f82409d3b5"
        }
    ]
}

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/gui/mainwindow/ItemListView.qml" line_range="86-94" />
<code_context>
+        if (popupIndex >= 0 && popupIndex < itemModel.count) {
+            var item = itemListView.itemAtIndex(popupIndex);
+            if (item) {
+                try {
+                    noteCtxMenu.popup(item, popupX, popupY);
+                    return;
+                } catch (e) {
+                }
+            }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid silently swallowing exceptions when popping the context menu.

The empty `catch` around `noteCtxMenu.popup(item, popupX, popupY)` masks runtime errors and complicates debugging. Please at least log the exception (e.g., `console.error(e)`) or rethrow in debug builds so issues are visible while still allowing the fallback `noteCtxMenu.popup()` path when needed.

```suggestion
        if (popupIndex >= 0 && popupIndex < itemModel.count) {
            var item = itemListView.itemAtIndex(popupIndex);
            if (item) {
                try {
                    noteCtxMenu.popup(item, popupX, popupY);
                    return;
                } catch (e) {
                    console.error("Failed to popup note context menu for index", popupIndex, "and noteIds", noteIds, e);
                }
            }
```
</issue_to_address>

### Comment 2
<location path="src/common/webrichetextmanager.cpp" line_range="238" />
<code_context>
     qInfo() << "Note update with result finished";
 }

+bool WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)
+{
+    bool clearedUpdateRequest = false;
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared state-reset and serial-checking logic into small helper methods to simplify and clarify update handling.

You can reduce the added complexity by consolidating state management and serial checking into a couple of small helpers.

### 1. Centralize update state reset

Right now `onUpdateNoteWithResult` and `clearTextChangeState` both reset `m_updateInProgress`, `m_updateRequestNoteId`, and `m_updateRequestSerial`. You can encapsulate this and remove duplication:

```cpp
void WebRichTextManager::resetUpdateRequestState()
{
    m_updateInProgress = false;
    m_updateRequestNoteId = -1;
    m_updateRequestSerial = 0;
}
```

Use it in `onUpdateNoteWithResult`:

```cpp
if (!data) {
    qWarning() << "onUpdateNoteWithResult called with null data, skip";
    resetUpdateRequestState();
    emit finishedUpdateNote();
    return;
}

// ...

if (data->noteId != m_textChangeNoteId || serial != m_textChangeSerial) {
    qWarning() << "Discard outdated note update result, noteId:" << data->noteId
               << "serial:" << serial
               << "textChangeNoteId:" << m_textChangeNoteId
               << "textChangeSerial:" << m_textChangeSerial;
    resetUpdateRequestState();
    emit finishedUpdateNote();
    return;
}

// ...

resetUpdateRequestState();
emit finishedUpdateNote();
```

And in `clearTextChangeState`:

```cpp
bool WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)
{
    bool clearedUpdateRequest = false;

    if (noteId == m_textChangeNoteId && serial == m_textChangeSerial) {
        m_textChange = false;
        m_textChangeNoteId = -1;
    }
    if (noteId == m_updateRequestNoteId && serial == m_updateRequestSerial) {
        resetUpdateRequestState();
        clearedUpdateRequest = true;
    }

    return clearedUpdateRequest;
}
```

This keeps all behavior but makes it obvious that there is a single way to “end” an update request.

### 2. Separate “clear state” from “control flow” in `clearTextChangeState`

To avoid `clearTextChangeState` doubling as a control flag, you can split responsibilities while preserving the current external behavior.

First, make `clearTextChangeState` only mutate text-change-related state:

```cpp
void WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)
{
    if (noteId == m_textChangeNoteId && serial == m_textChangeSerial) {
        m_textChange = false;
        m_textChangeNoteId = -1;
    }
}
```

Then add a dedicated helper for the update request:

```cpp
bool WebRichTextManager::clearUpdateRequestState(int noteId, quint64 serial)
{
    if (noteId == m_updateRequestNoteId && serial == m_updateRequestSerial) {
        resetUpdateRequestState();
        return true;
    }
    return false;
}
```

Update `flushNoteWithResult` to explicitly decide when to emit:

```cpp
void WebRichTextManager::flushNoteWithResult(VNoteItem *data, quint64 serial, const QString &result)
{
    if (!data) {
        return;
    }

    if (saveNoteWithResult(data, result)) {
        clearTextChangeState(data->noteId, serial);

        if (clearUpdateRequestState(data->noteId, serial)) {
            emit finishedUpdateNote();
        }
    }
}
```

This keeps all semantics intact but makes the control flow clearer: helpers only mutate state; the caller decides when to emit signals.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +86 to +94
if (popupIndex >= 0 && popupIndex < itemModel.count) {
var item = itemListView.itemAtIndex(popupIndex);
if (item) {
try {
noteCtxMenu.popup(item, popupX, popupY);
return;
} catch (e) {
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Avoid silently swallowing exceptions when popping the context menu.

The empty catch around noteCtxMenu.popup(item, popupX, popupY) masks runtime errors and complicates debugging. Please at least log the exception (e.g., console.error(e)) or rethrow in debug builds so issues are visible while still allowing the fallback noteCtxMenu.popup() path when needed.

Suggested change
if (popupIndex >= 0 && popupIndex < itemModel.count) {
var item = itemListView.itemAtIndex(popupIndex);
if (item) {
try {
noteCtxMenu.popup(item, popupX, popupY);
return;
} catch (e) {
}
}
if (popupIndex >= 0 && popupIndex < itemModel.count) {
var item = itemListView.itemAtIndex(popupIndex);
if (item) {
try {
noteCtxMenu.popup(item, popupX, popupY);
return;
} catch (e) {
console.error("Failed to popup note context menu for index", popupIndex, "and noteIds", noteIds, e);
}
}

qInfo() << "Note update with result finished";
}

bool WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting shared state-reset and serial-checking logic into small helper methods to simplify and clarify update handling.

You can reduce the added complexity by consolidating state management and serial checking into a couple of small helpers.

1. Centralize update state reset

Right now onUpdateNoteWithResult and clearTextChangeState both reset m_updateInProgress, m_updateRequestNoteId, and m_updateRequestSerial. You can encapsulate this and remove duplication:

void WebRichTextManager::resetUpdateRequestState()
{
    m_updateInProgress = false;
    m_updateRequestNoteId = -1;
    m_updateRequestSerial = 0;
}

Use it in onUpdateNoteWithResult:

if (!data) {
    qWarning() << "onUpdateNoteWithResult called with null data, skip";
    resetUpdateRequestState();
    emit finishedUpdateNote();
    return;
}

// ...

if (data->noteId != m_textChangeNoteId || serial != m_textChangeSerial) {
    qWarning() << "Discard outdated note update result, noteId:" << data->noteId
               << "serial:" << serial
               << "textChangeNoteId:" << m_textChangeNoteId
               << "textChangeSerial:" << m_textChangeSerial;
    resetUpdateRequestState();
    emit finishedUpdateNote();
    return;
}

// ...

resetUpdateRequestState();
emit finishedUpdateNote();

And in clearTextChangeState:

bool WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)
{
    bool clearedUpdateRequest = false;

    if (noteId == m_textChangeNoteId && serial == m_textChangeSerial) {
        m_textChange = false;
        m_textChangeNoteId = -1;
    }
    if (noteId == m_updateRequestNoteId && serial == m_updateRequestSerial) {
        resetUpdateRequestState();
        clearedUpdateRequest = true;
    }

    return clearedUpdateRequest;
}

This keeps all behavior but makes it obvious that there is a single way to “end” an update request.

2. Separate “clear state” from “control flow” in clearTextChangeState

To avoid clearTextChangeState doubling as a control flag, you can split responsibilities while preserving the current external behavior.

First, make clearTextChangeState only mutate text-change-related state:

void WebRichTextManager::clearTextChangeState(int noteId, quint64 serial)
{
    if (noteId == m_textChangeNoteId && serial == m_textChangeSerial) {
        m_textChange = false;
        m_textChangeNoteId = -1;
    }
}

Then add a dedicated helper for the update request:

bool WebRichTextManager::clearUpdateRequestState(int noteId, quint64 serial)
{
    if (noteId == m_updateRequestNoteId && serial == m_updateRequestSerial) {
        resetUpdateRequestState();
        return true;
    }
    return false;
}

Update flushNoteWithResult to explicitly decide when to emit:

void WebRichTextManager::flushNoteWithResult(VNoteItem *data, quint64 serial, const QString &result)
{
    if (!data) {
        return;
    }

    if (saveNoteWithResult(data, result)) {
        clearTextChangeState(data->noteId, serial);

        if (clearUpdateRequestState(data->noteId, serial)) {
            emit finishedUpdateNote();
        }
    }
}

This keeps all semantics intact but makes the control flow clearer: helpers only mutate state; the caller decides when to emit signals.

Bind editor flush requests to note id and change serial.

将编辑器 flush 请求绑定到笔记 ID 和变更序号。

Reuse flushed note content before context menu and save actions.

右键菜单和保存动作前复用已同步的笔记内容。

Log: 修复新建笔记快速右键保存菜单置灰问题
PMS: BUG-366545
Influence: 新建或编辑笔记后立即右键、快捷键弹出菜单或保存时,会先同步编辑器最新内容,避免保存笔记菜单误置灰或导出旧内容。
@github-actions

Copy link
Copy Markdown
  • 敏感词检查失败, 检测到1个文件存在敏感词
详情
{
    "src/common/VNoteMainManager.cpp": [
        {
            "line": "            url = \"https://www.deepin.org/zh/agreement/privacy/\";",
            "line_number": 1374,
            "rule": "S35",
            "reason": "Url link | 20e2eab189"
        },
        {
            "line": "            url = \"https://www.uniontech.com/agreement/privacy-cn\";",
            "line_number": 1376,
            "rule": "S35",
            "reason": "Url link | 4850a00dd7"
        },
        {
            "line": "            url = \"https://www.deepin.org/en/agreement/privacy/\";",
            "line_number": 1380,
            "rule": "S35",
            "reason": "Url link | 38d42f63bf"
        },
        {
            "line": "            url = \"https://www.uniontech.com/agreement/privacy-en\";",
            "line_number": 1382,
            "rule": "S35",
            "reason": "Url link | f82409d3b5"
        }
    ]
}

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:82分

■ 【总体评价】

代码通过引入序列号机制有效解决了笔记更新竞态问题,但右键菜单强刷机制引入了性能隐患
逻辑重构合理且无安全漏洞,因空异常捕获和潜在的性能卡顿扣18分

■ 【详细分析】

  • 1.语法逻辑(基本正确)✓

代码在VNoteMainManager和WebRichTextManager中正确传递和校验了serial参数,通过提取saveNoteWithResult和clearTextChangeState方法降低了原有函数的复杂度。但在ItemListView.qml的popupNoteContextMenu函数中,使用了空的try-catch块捕获异常且未做任何处理,这会掩盖潜在的UI渲染错误。此外,flushNoteWithResult与onUpdateNoteWithResult并行调用时,若序列号巧合匹配,clearTextChangeState可能会意外清除正在进行的正常更新请求状态。
潜在问题:空catch块导致异常被静默吞没;flush与正常更新流存在极端状态冲突可能
建议:在catch块中添加console.error或qWarning打印异常堆栈;在clearTextChangeState中增加更严格的状态机校验或加锁保护

  • 2.代码质量(良好)✓

代码遵循了Qt和QML的命名规范,将原有的庞大onUpdateNoteWithResult拆分为职责单一的私有方法,显著提升了可读性和可维护性。新增的flushCurrentNote函数封装了JS与C++的交互细节,使得QML层的调用更加简洁。但QML中存在多处相似的noteIds数组构建逻辑,依然存在一定程度的代码重复。
潜在问题:构建noteIds列表的循环逻辑在多处重复出现
建议:将构建noteIds列表的逻辑提取为ItemListView内的一个公共JavaScript函数,供右键菜单和多选操作复用

  • 3.代码性能(存在性能问题)✕

为了保证右键菜单弹出时数据已持久化,代码在popupNoteContextMenu和onSaveNote前强制调用了flushCurrentNote。该函数内部通过runJavaScript同步获取HTML,并触发数据库I/O操作。对于包含大量文本或复杂格式的笔记,getHtml()的序列化过程和随后的磁盘写入会导致主线程阻塞,造成右键菜单弹出和保存操作出现明显的卡顿延迟。
潜在问题:主线程阻塞导致UI掉帧;频繁的右键操作会引发不必要的全量数据库写入
建议:考虑将flushCurrentNote改为异步非阻塞模式,或在右键菜单场景下仅做轻量级校验而非强制全量刷新;在saveNoteWithResult中已有的htmlCode对比优化基础上,进一步在flushCurrentNote的JS层增加脏数据检查,避免无变更时的冗余调用

  • 4.代码安全(存在0个安全漏洞)✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个
本次代码变更主要涉及状态同步逻辑优化与序列号校验机制的引入。前端通过runJavaScript("getHtml()")获取的HTML内容直接作为参数传递给后端C++进行数据库更新。由于getHtml()为本地预置的JS函数,不受外部不可信输入控制,因此不存在跨站脚本注入(XSS)或HTML注入风险。序列号使用quint64类型且仅在内部传递,不存在整数溢出被利用的风险。
建议:继续保持对前端JS交互接口的隔离,避免未来将外部不可信数据直接拼接进runJavaScript的脚本字符串中

■ 【改进建议代码示例】

// src/gui/mainwindow/ItemListView.qml
    function popupNoteContextMenu(noteIds, popupIndex, popupX, popupY) {
        if (!noteIds || noteIds.length === 0)
            return;

        VNoteMainManager.checkNoteVoice(noteIds);
        VNoteMainManager.checkNoteText(noteIds);
        if (popupIndex >= 0 && popupIndex < itemModel.count) {
            var item = itemListView.itemAtIndex(popupIndex);
            if (item) {
                try {
                    noteCtxMenu.popup(item, popupX, popupY);
                    return;
                } catch (e) {
                    // 修复:增加异常日志记录,避免静默吞没错误
                    console.error("Failed to popup context menu at specific item:", e.message);
                }
            }
        }
        noteCtxMenu.popup();
    }

@dengzhongyuan365-dev dengzhongyuan365-dev marked this pull request as draft June 22, 2026 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants