diff --git a/src/common/VNoteMainManager.cpp b/src/common/VNoteMainManager.cpp index 749ea4c4..225becfa 100644 --- a/src/common/VNoteMainManager.cpp +++ b/src/common/VNoteMainManager.cpp @@ -127,6 +127,7 @@ void VNoteMainManager::initConnections() connect(m_richTextManager, &WebRichTextManager::noteTextChanged, this, &VNoteMainManager::onNoteChanged, Qt::QueuedConnection); connect(m_richTextManager, &WebRichTextManager::updateSearch, this, &VNoteMainManager::updateSearch); connect(m_richTextManager, &WebRichTextManager::scrollChange, this, &VNoteMainManager::scrollChange); + connect(TiptapChannelBridge::instance(), &TiptapChannelBridge::scrollChanged, this, &VNoteMainManager::scrollChange); connect(m_richTextManager, &WebRichTextManager::finishedUpdateNote, this, &VNoteMainManager::onRichTextSaveFinished); connect(VoiceRecoderHandler::instance(), &VoiceRecoderHandler::finishedRecod, this, &VNoteMainManager::insertVoice); qInfo() << "Connections initialized"; diff --git a/src/common/tiptapchannelbridge.cpp b/src/common/tiptapchannelbridge.cpp index d38afadc..11532a37 100644 --- a/src/common/tiptapchannelbridge.cpp +++ b/src/common/tiptapchannelbridge.cpp @@ -306,6 +306,16 @@ void TiptapChannelBridge::jsPasteImage(const QString &dataUrl) emit insertImage(QJsonDocument(info).toJson(QJsonDocument::Compact)); } +// --------------------------------------------------------------------------- +// 滚动位置上报(JS→C++) +// --------------------------------------------------------------------------- + +void TiptapChannelBridge::jsReportScroll(int scrollTop) +{ + const bool isTop = (scrollTop <= 0); + emit scrollChanged(isTop); +} + // --------------------------------------------------------------------------- // Voice 播放/转写入口(JS→C++) diff --git a/src/common/tiptapchannelbridge.h b/src/common/tiptapchannelbridge.h index 74e26a35..79558c9b 100644 --- a/src/common/tiptapchannelbridge.h +++ b/src/common/tiptapchannelbridge.h @@ -94,6 +94,9 @@ class TiptapChannelBridge : public QObject // 前端粘贴剪贴板图片数据(data URL),宿主保存到 images/ 后回插 Q_INVOKABLE void jsPasteImage(const QString &dataUrl); + // 前端上报编辑器滚动位置(scrollTop),宿主据此驱动标题栏阴影状态 + Q_INVOKABLE void jsReportScroll(int scrollTop); + // 宿主侧下发字体列表(未就绪时缓存,就绪后补发) Q_INVOKABLE void sendFontList(const QStringList &fonts, const QString &defaultFont); @@ -172,6 +175,9 @@ class TiptapChannelBridge : public QObject void themeProvided(const QString &theme, const QString &highlightColor, const QString &disableHighlightColor, const QString &backgroundColor); + // JS→C++:滚动位置上报(isTop=true 表示已滚到顶部) + void scrollChanged(bool isTop); + // C++ 内部请求信号(WebEngineHandler 连接处理) void voicePlaybackRequested(const QString &voiceInfoJson, bool isSame); void voicePlaybackStopRequested(); diff --git a/src/gui/mainwindow/WebEngineView.qml b/src/gui/mainwindow/WebEngineView.qml index 52088981..8166a19a 100644 --- a/src/gui/mainwindow/WebEngineView.qml +++ b/src/gui/mainwindow/WebEngineView.qml @@ -451,7 +451,11 @@ Item { messageDialogLoader.showDialog(type); } onTriggerWebAction: action => { - webView.triggerWebAction(action); + if (TiptapChannel.debugEnabled && tiptapLoader.item) { + tiptapWebView.triggerWebAction(action); + } else { + webView.triggerWebAction(action); + } } onViewPicture: filePath => { viewPictureLoader.path = filePath; @@ -501,6 +505,34 @@ Item { tiptapWebView.webChannel = tiptapWebChannel; tiptapWebView.url = Qt.resolvedUrl(TiptapChannel.tiptapHtmlPath()); } + + onContextMenuRequested: req => { + req.accepted = true; + var rawX = Number(req.position.x); + var rawY = Number(req.position.y); + if (isNaN(rawX) || isNaN(rawY)) return; + var sx = String(Math.round(rawX)); + var sy = String(Math.round(rawY)); + tiptapWebView.runJavaScript( + "(function(){" + + "var el=document.elementFromPoint(" + sx + "," + sy + ");" + + "if(!el) return JSON.stringify({type:2,json:''});" + + "var vb=el.closest?el.closest('.voiceInfoBox'):null;" + + "if(vb&&vb.getAttribute('data-type')==='voice-block'){" + + "return JSON.stringify({type:1,json:vb.getAttribute('data-voice-meta')||''});}" + + "var img=el.closest?el.closest('img[data-rel-path]'):null;" + + "if(img) return JSON.stringify({type:0,json:''});" + + "return JSON.stringify({type:2,json:''});" + + "})()", + function(result) { + var info = null; + try { info = JSON.parse(result); } catch(e) {} + if (!info) return; + if (info.type === 0) return; + handler.onSaveMenuParam(info.type, info.json); + handler.onContextMenuRequested(req); + }); + } } DropArea { @@ -807,7 +839,11 @@ Item { hasScroll = !isTop; } onUpdateRichTextSearch: key => { - webView.findText(key); + if (TiptapChannel.debugEnabled && tiptapLoader.item) { + tiptapWebView.findText(key); + } else { + webView.findText(key); + } } } @@ -871,4 +907,27 @@ Item { } } } + + Connections { + target: Webobj + + onCallJsSelectAll: { + if (TiptapChannel.debugEnabled && tiptapLoader.item) { + tiptapWebView.runJavaScript( + "if(window.__dvnTiptapEditor)window.__dvnTiptapEditor.chain().selectAll().run()"); + } + } + onCallJsDeleteSelection: { + if (TiptapChannel.debugEnabled && tiptapLoader.item) { + tiptapWebView.runJavaScript( + "if(window.__dvnTiptapEditor)window.__dvnTiptapEditor.chain().deleteSelection().run()"); + } + } + onCallJsFocusEditor: { + if (TiptapChannel.debugEnabled && tiptapLoader.item) { + tiptapWebView.runJavaScript( + "if(window.__dvnTiptapEditor)window.__dvnTiptapEditor.commands.focus()"); + } + } + } } diff --git a/src/handler/web_engine_handler.cpp b/src/handler/web_engine_handler.cpp index a9994a00..3e2f6b27 100644 --- a/src/handler/web_engine_handler.cpp +++ b/src/handler/web_engine_handler.cpp @@ -741,8 +741,10 @@ void WebEngineHandler::processVoiceMenuRequest(QObject *request) // 异步操作,防止阻塞前端事件 QTimer::singleShot(0, this, [this] { Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::VoicePathNoAvail); - // 调用 js 删除删除语音文本 - Q_EMIT JsContent::instance()->callJsDeleteSelection(); + // 调试态下 Tiptap 编辑器不走 Summernote JS 删除路径 + if (!TiptapChannelBridge::instance()->debugEnabled()) { + Q_EMIT JsContent::instance()->callJsDeleteSelection(); + } }); return; } @@ -777,8 +779,10 @@ void WebEngineHandler::processVoiceMenuRequest(QWebEngineContextMenuRequest *req // 异步操作,防止阻塞前端事件 QTimer::singleShot(0, this, [this] { Q_EMIT requestMessageDialog(VNoteMessageDialogHandler::VoicePathNoAvail); - // 调用 js 删除删除语音文本 - Q_EMIT JsContent::instance()->callJsDeleteSelection(); + // 调试态下 Tiptap 编辑器不走 Summernote JS 删除路径 + if (!TiptapChannelBridge::instance()->debugEnabled()) { + Q_EMIT JsContent::instance()->callJsDeleteSelection(); + } }); return; } diff --git a/tests/src/tiptapchannel/ut_tiptapchannelbridge.cpp b/tests/src/tiptapchannel/ut_tiptapchannelbridge.cpp index db01b711..e211ef35 100644 --- a/tests/src/tiptapchannel/ut_tiptapchannelbridge.cpp +++ b/tests/src/tiptapchannel/ut_tiptapchannelbridge.cpp @@ -515,3 +515,27 @@ TEST_F(UT_TiptapChannelBridge, UT_TiptapChannelBridge_currentVoiceId_001) bridge.setCurrentVoiceId(QStringLiteral("voice-x")); EXPECT_EQ(bridge.currentVoiceId(), QStringLiteral("voice-x")); } + +// --------------------------------------------------------------------------- +// 滚动位置上报(JS→C++):jsReportScroll + scrollChanged 信号 +// --------------------------------------------------------------------------- + +// jsReportScroll(0) → scrollChanged(true)(已到顶部) +TEST_F(UT_TiptapChannelBridge, UT_TiptapChannelBridge_jsReportScroll_top_001) +{ + TiptapChannelBridge bridge; + QSignalSpy spy(&bridge, &TiptapChannelBridge::scrollChanged); + bridge.jsReportScroll(0); + ASSERT_EQ(spy.count(), 1); + EXPECT_EQ(spy.takeFirst().at(0).toBool(), true); +} + +// jsReportScroll(正值) → scrollChanged(false)(未到顶部) +TEST_F(UT_TiptapChannelBridge, UT_TiptapChannelBridge_jsReportScroll_scrolled_001) +{ + TiptapChannelBridge bridge; + QSignalSpy spy(&bridge, &TiptapChannelBridge::scrollChanged); + bridge.jsReportScroll(150); + ASSERT_EQ(spy.count(), 1); + EXPECT_EQ(spy.takeFirst().at(0).toBool(), false); +}