diff --git a/CMakeLists.txt b/CMakeLists.txt index dd63cbc1797fe9777bd30a8a6d4308de0cd8f693..6563bc457e9ff2c5d18937a6e64cab3b51c38119 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ configure_file(src/environments.h.in environments.h @ONLY) file(GLOB SRCS "src/*.h" "src/*.cpp" "src/thememodule/*.h" "src/thememodule/*.cpp" "src/widgets/*.h" "src/widgets/*.cpp" - "src/wayland/kywc-toplevel-v1-protocol.c" + "src/wayland/kywc-toplevel-v1-protocol.c" "src/wayland/*.cpp" "src/wayland/*.h") # Find the library @@ -45,29 +45,30 @@ find_package(Qt6Concurrent REQUIRED) find_package(Qt6PrintSupport REQUIRED) find_package(Qt6Gui REQUIRED) find_package(Qt6WaylandClient REQUIRED) -find_package(Dtk2widget REQUIRED) +find_package(Dtk6Widget REQUIRED) find_package(Dtk6Core REQUIRED) find_package(KF6SyntaxHighlighting REQUIRED) find_package(KF6Codecs REQUIRED) # 尝试查找WebEngine模块 -find_package(Qt6WebEngine QUIET) +find_package(Qt6WebEngineCore QUIET) find_package(Qt6WebEngineWidgets QUIET) +find_package(Qt6WebChannel QUIET) include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) # Tell CMake to create the executable add_executable(gxde-editor ${SRCS} src/gxde-editor.qrc) -target_include_directories(gxde-editor PUBLIC - /usr/include/dtk2/DWidget - /usr/include/dtk6/DCore - /usr/include/dtk6/DLog +target_include_directories(gxde-editor PUBLIC + /usr/include/dtk6/DWidget + /usr/include/dtk6/DCore + /usr/include/dtk6/DLog /usr/include/dtk6/DGui - ${OBJECT_BINARY_DIR} + ${OBJECT_BINARY_DIR} ${CMARK_INCLUDE_DIRS} ) target_link_libraries(gxde-editor - ${Dtk2widget_LIBRARIES} + Dtk6::Widget Dtk6::Core Qt6::Widgets Qt6::DBus @@ -83,11 +84,12 @@ target_link_libraries(gxde-editor ) # 判断 Qt6WebEngine 模块是否存在 -if(Qt6WebEngine_FOUND AND Qt6WebEngineWidgets_FOUND) +if(Qt6WebEngineCore_FOUND AND Qt6WebEngineWidgets_FOUND AND Qt6WebChannel_FOUND) # 添加模块链接和宏定义 target_link_libraries(gxde-editor - Qt6::WebEngine + Qt6::WebEngineCore Qt6::WebEngineWidgets + Qt6::WebChannel ) target_compile_definitions(gxde-editor PRIVATE diff --git a/debian/control b/debian/control index 6fc596a98d1edbfe476c152ca2b729b9c708e663..abc1d837c8b135a4b8d156f33c07b93025767f91 100644 --- a/debian/control +++ b/debian/control @@ -4,19 +4,19 @@ Priority: optional Maintainer: gfdgd xi <3025613752@qq.com>, Deepin Packages Builder Build-Depends: debhelper (>= 9), - cmake, + cmake, pkg-config, - libdtk6core-dev, - libdtk2widget6-dev, - qt6-base-dev, - libpolkit-qt6-1-dev, - libkf6syntaxhighlighting-dev, - libkf6codecs-dev, + libdtk6core-dev, + libdtk6widget-dev, + qt6-base-dev, + libpolkit-qt6-1-dev, + libkf6syntaxhighlighting-dev, + libkf6codecs-dev, qt6-tools-dev-tools, qt6-base-private-dev, qt6-5compat-dev, - qt6-wayland-dev -# qt6-webengine-dev [i386 amd64 armhf arm64 mips64el loong64] + qt6-wayland-dev, + qt6-webengine-dev, Standards-Version: 3.9.8 Homepage: https://www.gxde.top diff --git a/src/editwrapper.cpp b/src/editwrapper.cpp index e73a12a58136f7508377e2e9a14aac02a15719b5..58a0248565645c0359254bdedcfe97564be98745 100644 --- a/src/editwrapper.cpp +++ b/src/editwrapper.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include "drecentmanager.h" @@ -101,7 +102,7 @@ EditWrapper::EditWrapper(QWidget *parent) connect(m_toast, &Toast::saveAsBtnClicked, this, &EditWrapper::requestSaveAs); connect(m_pendingLoadTimer, &QTimer::timeout, this, &EditWrapper::appendPendingTextLoadChunk); - setDarkTheme(DThemeManager::instance()->theme() == "dark"); + setDarkTheme(Dtk::Gui::DGuiApplicationHelper::instance()->themeType() == Dtk::Gui::DGuiApplicationHelper::DarkType); } EditWrapper::~EditWrapper() diff --git a/src/findbar.cpp b/src/findbar.cpp index 83e7cef0698ee38d085f73b2227dec0fe71710d3..a542e8105d21b2d0a344308a745f44a589550b51 100644 --- a/src/findbar.cpp +++ b/src/findbar.cpp @@ -25,6 +25,7 @@ #include #include +#include FindBar::FindBar(QWidget *parent) : QWidget(parent) @@ -39,7 +40,7 @@ FindBar::FindBar(QWidget *parent) m_editLine = new LineBar(); m_findNextButton = new QPushButton(tr("Next")); m_findPrevButton = new QPushButton(tr("Previous")); - m_closeButton = new DImageButton(); + m_closeButton = new QPushButton(); m_closeButton->setFixedSize(16, 16); m_layout->addWidget(m_findLabel); @@ -61,7 +62,7 @@ FindBar::FindBar(QWidget *parent) connect(m_findNextButton, &QPushButton::clicked, this, &FindBar::findNext, Qt::QueuedConnection); connect(m_findPrevButton, &QPushButton::clicked, this, &FindBar::findPrev, Qt::QueuedConnection); - connect(m_closeButton, &DImageButton::clicked, this, &FindBar::findCancel, Qt::QueuedConnection); + connect(m_closeButton, &QPushButton::clicked, this, &FindBar::findCancel, Qt::QueuedConnection); } bool FindBar::isFocus() @@ -72,15 +73,15 @@ bool FindBar::isFocus() void FindBar::focus() { m_editLine->setFocus(); - m_editLine->selectAll(); + m_editLine->lineEdit()->selectAll(); } void FindBar::activeInput(QString text, QString file, int row, int column, int scrollOffset) { // Try fill keyword with select text. m_editLine->clear(); - m_editLine->insert(text); - m_editLine->selectAll(); + m_editLine->lineEdit()->insert(text); + m_editLine->lineEdit()->selectAll(); // Show. QWidget::show(); @@ -143,15 +144,13 @@ void FindBar::setBackground(QString color) if (QColor(m_backgroundColor).lightness() < 128) { m_findLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#AAAAAA")); - m_closeButton->setNormalPic(Utils::getQrcPath("bar_close_normal_dark.svg")); - m_closeButton->setHoverPic(Utils::getQrcPath("bar_close_hover_dark.svg")); - m_closeButton->setPressPic(Utils::getQrcPath("bar_close_press_dark.svg")); + m_closeButton->setIcon(QIcon(Utils::getQrcPath("bar_close_normal_dark.svg"))); + m_closeButton->setStyleSheet("QPushButton { border: none; }"); } else { m_findLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#000000")); - m_closeButton->setNormalPic(Utils::getQrcPath("bar_close_normal_light.svg")); - m_closeButton->setHoverPic(Utils::getQrcPath("bar_close_hover_light.svg")); - m_closeButton->setPressPic(Utils::getQrcPath("bar_close_press_light.svg")); + m_closeButton->setIcon(QIcon(Utils::getQrcPath("bar_close_normal_light.svg"))); + m_closeButton->setStyleSheet("QPushButton { border: none; }"); } update(); diff --git a/src/findbar.h b/src/findbar.h index ae0bd7ef043cc213766f591eedf52ec2aebe6ea9..b2d39f81f23a191408395d2ad151468f80ae40de 100644 --- a/src/findbar.h +++ b/src/findbar.h @@ -30,7 +30,7 @@ #include #include #include -#include +#include DWIDGET_USE_NAMESPACE @@ -67,7 +67,7 @@ protected: private: QPushButton *m_findNextButton; QPushButton *m_findPrevButton; - DImageButton *m_closeButton; + QPushButton *m_closeButton; LineBar *m_editLine; QHBoxLayout *m_layout; QLabel *m_findLabel; diff --git a/src/jumplinebar.cpp b/src/jumplinebar.cpp index a5eb7cd7ffb6f545712c6000c7745bd9c2a1eca8..d9b94ccbac1432a89a63f661e94c212d0c3a7c41 100644 --- a/src/jumplinebar.cpp +++ b/src/jumplinebar.cpp @@ -41,7 +41,7 @@ JumpLineBar::JumpLineBar(QWidget *parent) m_editLine = new LineBar(); m_lineValidator = new QIntValidator; - m_editLine->setValidator(m_lineValidator); + m_editLine->lineEdit()->setValidator(m_lineValidator); m_layout->addWidget(m_label); m_layout->addWidget(m_editLine); diff --git a/src/main.cpp b/src/main.cpp index 70e73e6841e10c0223f8af8dfd1b7e16ac5a925b..79625939120b91b5206eef2c2f6a9831d030ffa5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,8 +44,6 @@ DTK_USE_NAMESPACE int main(int argc, char *argv[]) { - // Init DTK. - DApplication::loadDXcbPlugin(); const char *descriptionText = QT_TRANSLATE_NOOP("MainWindow", "GXDE Editor is a desktop text editor that supports common text editing features."); diff --git a/src/replacebar.cpp b/src/replacebar.cpp index bec03fc067744e6b1910d768b5536cd6b59006f1..f96f044686de56e583d2926d97b642a7fbcba415 100644 --- a/src/replacebar.cpp +++ b/src/replacebar.cpp @@ -25,6 +25,7 @@ #include #include +#include ReplaceBar::ReplaceBar(QWidget *parent) : QWidget(parent) @@ -43,7 +44,7 @@ ReplaceBar::ReplaceBar(QWidget *parent) m_replaceSkipButton = new QPushButton(tr("Skip")); m_replaceRestButton = new QPushButton(tr("Replace Rest")); m_replaceAllButton = new QPushButton(tr("Replace All")); - m_closeButton = new DImageButton(); + m_closeButton = new QPushButton(); m_closeButton->setFixedSize(16, 16); m_layout->addWidget(m_replaceLabel); @@ -85,7 +86,7 @@ ReplaceBar::ReplaceBar(QWidget *parent) connect(m_replaceRestButton, &QPushButton::clicked, this, &ReplaceBar::handleReplaceRest, Qt::QueuedConnection); connect(m_replaceAllButton, &QPushButton::clicked, this, &ReplaceBar::handleReplaceAll, Qt::QueuedConnection); - connect(m_closeButton, &DImageButton::clicked, this, &ReplaceBar::replaceCancel, Qt::QueuedConnection); + connect(m_closeButton, &QPushButton::clicked, this, &ReplaceBar::replaceCancel, Qt::QueuedConnection); } bool ReplaceBar::isFocus() @@ -102,8 +103,8 @@ void ReplaceBar::activeInput(QString text, QString file, int row, int column, in { // Try fill keyword with select text. m_replaceLine->clear(); - m_replaceLine->insert(text); - m_replaceLine->selectAll(); + m_replaceLine->lineEdit()->insert(text); + m_replaceLine->lineEdit()->selectAll(); // Show. show(); @@ -176,16 +177,14 @@ void ReplaceBar::setBackground(QString color) m_replaceLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#AAAAAA")); m_withLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#AAAAAA")); - m_closeButton->setNormalPic(Utils::getQrcPath("bar_close_normal_dark.svg")); - m_closeButton->setHoverPic(Utils::getQrcPath("bar_close_hover_dark.svg")); - m_closeButton->setPressPic(Utils::getQrcPath("bar_close_press_dark.svg")); + m_closeButton->setIcon(QIcon(Utils::getQrcPath("bar_close_normal_dark.svg"))); + m_closeButton->setStyleSheet("QPushButton { border: none; }"); } else { m_replaceLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#000000")); m_withLabel->setStyleSheet(QString("QLabel { background-color: %1; color: %2; }").arg(color).arg("#000000")); - m_closeButton->setNormalPic(Utils::getQrcPath("bar_close_normal_light.svg")); - m_closeButton->setHoverPic(Utils::getQrcPath("bar_close_hover_light.svg")); - m_closeButton->setPressPic(Utils::getQrcPath("bar_close_press_light.svg")); + m_closeButton->setIcon(QIcon(Utils::getQrcPath("bar_close_normal_light.svg"))); + m_closeButton->setStyleSheet("QPushButton { border: none; }"); } repaint(); diff --git a/src/replacebar.h b/src/replacebar.h index 32c17ff43de5efa6765714354a1e69a3206b42cb..2c63c03baaac439dea67ab5448eccd1beed3aaa0 100644 --- a/src/replacebar.h +++ b/src/replacebar.h @@ -30,7 +30,7 @@ #include #include #include -#include +#include DWIDGET_USE_NAMESPACE @@ -77,7 +77,7 @@ private: QPushButton *m_replaceButton; QPushButton *m_replaceRestButton; QPushButton *m_replaceSkipButton; - DImageButton *m_closeButton; + QPushButton *m_closeButton; LineBar *m_replaceLine; LineBar *m_withLine; QHBoxLayout *m_layout; diff --git a/src/settings.cpp b/src/settings.cpp index 2cbb2d641d88684816b5f22bbe4c22210f47dc84..842deb5a605a2ea2010a5347b14285a6bc90ad8c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -144,7 +145,13 @@ QWidget *Settings::createFontComBoBoxHandle(QObject *obj) auto option = qobject_cast(obj); QComboBox *comboBox = new QComboBox; - QWidget *optionWidget = DSettingsWidgetFactory::createTwoColumWidget(option, comboBox); + const auto pair = DSettingsWidgetFactory::createStandardItem(QByteArray(), option, comboBox); + QWidget *optionWidget = new QWidget; + QHBoxLayout *layout = new QHBoxLayout(optionWidget); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(pair.first); + layout->addWidget(pair.second); QFontDatabase fontDatabase; comboBox->addItems(fontDatabase.families()); diff --git a/src/widgets/bottombar.cpp b/src/widgets/bottombar.cpp index 66600b6313cd1663fd3e6488d19ec9d6c00b7a6a..df13d220bafc7ebbb99ac0f72cb15eaaef2fd72d 100644 --- a/src/widgets/bottombar.cpp +++ b/src/widgets/bottombar.cpp @@ -22,6 +22,7 @@ #include "../editwrapper.h" #include #include +#include #include #include diff --git a/src/widgets/markdownpreviewwidget.cpp b/src/widgets/markdownpreviewwidget.cpp index 5ea6b6c1ef664c2f6b7381d450f948aaab8beac3..f27d9cefec8284543c5ca72bb4a789fd1351c624 100644 --- a/src/widgets/markdownpreviewwidget.cpp +++ b/src/widgets/markdownpreviewwidget.cpp @@ -4,15 +4,13 @@ #include #include #include +#include #include -#include -#include #include #include #include - -#include -#include +#include +#include // 使用GitHub风格的Markdown样式 const QString STYLE_LIGHT = R"( @@ -43,19 +41,18 @@ const QString STYLE_DARK = R"( )"; -// JavaScript 同步代码 -const QString SCROLL_SYNC_JS = R"( -let lastSentPosition = 0; -const SYNC_THRESHOLD = 5; // 像素阈值避免微小抖动 +MarkdownPreviewBridge::MarkdownPreviewBridge(MarkdownPreviewWidget* widget) + : QObject(widget) + , m_widget(widget) +{ +} -window.addEventListener('scroll', function(e) { - const current = window.scrollY || document.documentElement.scrollTop; - if (Math.abs(current - lastSentPosition) > SYNC_THRESHOLD) { - lastSentPosition = current; - window.external.syncPreviewScroll(current); +void MarkdownPreviewBridge::syncPreviewToEditor(double ratio) +{ + if (m_widget) { + m_widget->syncPreviewToEditor(ratio); } -}); -)"; +} MarkdownPreviewWidget::MarkdownPreviewWidget(QWidget* parent) : QWidget(parent) @@ -65,6 +62,10 @@ MarkdownPreviewWidget::MarkdownPreviewWidget(QWidget* parent) m_webView = new QWebEngineView(this); m_webPage = new MarkdownWebPage(this); + m_webChannel = new QWebChannel(this); + m_bridge = new MarkdownPreviewBridge(this); + m_webChannel->registerObject(QStringLiteral("markdownPreview"), m_bridge); + m_webPage->setWebChannel(m_webChannel); m_webView->setPage(m_webPage); m_webView->settings()->setAttribute(QWebEngineSettings::WebAttribute::LocalContentCanAccessRemoteUrls, true); @@ -78,132 +79,177 @@ MarkdownPreviewWidget::MarkdownPreviewWidget(QWidget* parent) m_updateTimer->setSingleShot(true); connect(m_updateTimer, &QTimer::timeout, this, &MarkdownPreviewWidget::performUpdate); + // 定时轮询预览区滚动位置,用于预览 -> 编辑器的同步 m_scrollSyncTimer = new QTimer(this); - m_scrollSyncTimer->setSingleShot(true); - //connect(m_scrollSyncTimer, &QTimer::timeout, this, &MarkdownPreviewWidget::setupScrollSync); + m_scrollSyncTimer->setInterval(50); + connect(m_scrollSyncTimer, &QTimer::timeout, this, &MarkdownPreviewWidget::pollPreviewScroll); + + // 页面加载完成后恢复滚动比例并开始监听预览区滚动 + connect(m_webView, &QWebEngineView::loadFinished, + this, &MarkdownPreviewWidget::onPreviewLoadFinished); } bool MarkdownPreviewWidget::isSupport() { - // 检测是否是 root 用户且有设置 QTWEBENGINE_DISABLE_SANDBOX - if (QProcessEnvironment::systemEnvironment().value("USER") != "root" || - QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_DISABLE_SANDBOX") == "1") { - QDBusMessage dbus = QDBusMessage::createMethodCall("com.gxde.daemon.system.info", - "/com/gxde/daemon/system/info", - "com.gxde.daemon.system.info", - "IsInChroot"); - auto result = QDBusConnection::sessionBus().call(dbus); - bool isInChroot = false; - if (result.arguments().count() > 0) { - isInChroot = result.arguments().at(0).toBool(); - } - if (!isInChroot) { - return 1; - } - } - return 0; + // 只要编译期启用了 WebEngine,就启用 Markdown 预览。 + // 之前还会根据 root/chroot 环境禁用,导致部分环境下看不到预览界面。 + return true; } -void MarkdownPreviewWidget::setSourceEditor(QTextEdit* editor) { - if (m_sourceEditor == editor) return; +void MarkdownPreviewWidget::setSourceEditor(QTextEdit* editor) +{ + if (m_sourceEditor == editor) + return; if (m_sourceEditor) { disconnect(m_sourceEditor, &QTextEdit::textChanged, this, &MarkdownPreviewWidget::scheduleUpdate); - } - - if (m_sourceEditor) { disconnect(m_sourceEditor->verticalScrollBar(), &QScrollBar::valueChanged, - this, &MarkdownPreviewWidget::syncEditorToPreview); + this, &MarkdownPreviewWidget::syncEditorToPreview); } m_sourceEditor = editor; + ++m_loadGeneration; + m_syncing = false; + m_lastScrollRatio = 0.0; + m_lastPreviewRatio = 0.0; - if (m_sourceEditor) { - connect(m_sourceEditor, &QTextEdit::textChanged, - this, &MarkdownPreviewWidget::scheduleUpdate); - scheduleUpdate(); - } + if (m_scrollSyncTimer) + m_scrollSyncTimer->stop(); - // 初始化滚动同步 - if (m_sourceEditor) { - connect(m_sourceEditor->verticalScrollBar(), &QScrollBar::valueChanged, - this, &MarkdownPreviewWidget::syncEditorToPreview); - // 初始化内容 - m_webView->setHtml(generateHtml(m_sourceEditor->toPlainText())); + if (!m_sourceEditor) { + m_webView->setHtml(QString()); + return; } -} -void MarkdownPreviewWidget::setupScrollSync() { - // 注入JavaScript同步处理器 - QWebEngineScript script; - script.setSourceCode(SCROLL_SYNC_JS); - script.setName("ScrollSync"); - script.setWorldId(QWebEngineScript::MainWorld); - script.setInjectionPoint(QWebEngineScript::DocumentReady); - m_webView->page()->scripts().insert(script); + connect(m_sourceEditor, &QTextEdit::textChanged, + this, &MarkdownPreviewWidget::scheduleUpdate); + connect(m_sourceEditor->verticalScrollBar(), &QScrollBar::valueChanged, + this, &MarkdownPreviewWidget::syncEditorToPreview); + + // 初始化内容,页面加载完成后会恢复滚动位置 + m_webView->setHtml(generateHtml(m_sourceEditor->toPlainText()), QUrl("qrc:/")); } -double MarkdownPreviewWidget::calculateSyncRatio() { - const int editorHeight = m_sourceEditor->height(); - const int editorContentHeight = m_sourceEditor->document()->size().height(); - const double editorVisibleRatio = static_cast(editorHeight) / editorContentHeight; +void MarkdownPreviewWidget::syncEditorToPreview() +{ + if (!m_sourceEditor || m_syncing) + return; - // 获取预览内容高度 - int previewContentHeight = 0; - m_webView->page()->runJavaScript("document.documentElement.scrollHeight", - [&](const QVariant& result) { - previewContentHeight = result.toInt(); - }); + QScrollBar* scrollBar = m_sourceEditor->verticalScrollBar(); + if (scrollBar->maximum() <= 0) + return; - const int previewHeight = m_webView->height(); - const double previewVisibleRatio = static_cast(previewHeight) / previewContentHeight; + const double ratio = static_cast(scrollBar->value()) / scrollBar->maximum(); + m_lastPreviewRatio = ratio; + + m_syncing = true; + setPreviewScrollRatio(ratio); - // 计算比例修正系数(考虑padding等因素) - return (1.0 - previewVisibleRatio) / (1.0 - editorVisibleRatio); + // 短暂锁定,避免 JS 侧回读滚动位置再次触发编辑器滚动 + QTimer::singleShot(100, this, [this] { + m_syncing = false; + }); } -void MarkdownPreviewWidget::syncEditorToPreview() { - //if (m_syncing) return; - m_syncing = true; +void MarkdownPreviewWidget::syncPreviewToEditor(double ratio) +{ + if (!m_sourceEditor || m_syncing) + return; - // 计算滚动比例 QScrollBar* scrollBar = m_sourceEditor->verticalScrollBar(); - const double ratio = static_cast(scrollBar->value()) / scrollBar->maximum(); + if (scrollBar->maximum() <= 0) + return; - // 异步执行滚动操作 - m_webView->page()->runJavaScript( - QString("window.scrollTo(0, document.documentElement.scrollHeight * %1);") - .arg(ratio) - ); + m_lastPreviewRatio = ratio; + m_syncing = true; + scrollBar->setValue(qRound(ratio * scrollBar->maximum())); + + QTimer::singleShot(100, this, [this] { + m_syncing = false; + }); +} + +void MarkdownPreviewWidget::pollPreviewScroll() +{ + if (!m_sourceEditor || m_syncing || !m_webView) + return; - // 200ms后解除同步锁定 - //QTimer::singleShot(200, this, [this](){ m_syncing = false; }); + const int generation = m_loadGeneration; + m_webView->page()->runJavaScript( + "var e = document.documentElement;" + "var h = e.scrollHeight - e.clientHeight;" + "h > 0 ? (window.scrollY || e.scrollTop || 0) / h : 0;", + [this, generation](const QVariant &result) { + if (generation != m_loadGeneration || !m_sourceEditor || m_syncing) + return; + + const double ratio = result.toDouble(); + if (qAbs(ratio - m_lastPreviewRatio) < 0.0005) + return; + + m_lastPreviewRatio = ratio; + + QScrollBar* scrollBar = m_sourceEditor->verticalScrollBar(); + if (scrollBar->maximum() <= 0) + return; + + m_syncing = true; + scrollBar->setValue(qRound(ratio * scrollBar->maximum())); + QTimer::singleShot(100, this, [this] { + m_syncing = false; + }); + }); } +void MarkdownPreviewWidget::onPreviewLoadFinished(bool ok) +{ + if (!ok || !m_sourceEditor) + return; + + // 在 JS 真正执行 scrollTo 后再记录比例并启动轮询, + // 避免轮询读到恢复前的旧位置导致滚动被拉回去。 + const double ratio = m_lastScrollRatio; + const int generation = m_loadGeneration; + m_webView->page()->runJavaScript( + QString("var e = document.documentElement;" + "var h = e.scrollHeight - e.clientHeight;" + "window.scrollTo(0, %1 * (h > 0 ? h : 0));").arg(ratio), + [this, ratio, generation](const QVariant &) { + if (generation != m_loadGeneration) + return; + m_lastPreviewRatio = ratio; + if (m_sourceEditor && m_scrollSyncTimer && !m_scrollSyncTimer->isActive()) + m_scrollSyncTimer->start(); + } + ); +} -void MarkdownPreviewWidget::scheduleUpdate() { - m_lastScrollPosition = m_webView->page()->scrollPosition().y(); +void MarkdownPreviewWidget::scheduleUpdate() +{ + // 记录当前预览比例,内容刷新后尽量维持阅读位置 + m_lastScrollRatio = m_lastPreviewRatio; m_updateTimer->start(200); // 200ms防抖 } -void MarkdownPreviewWidget::performUpdate() { - if (!m_sourceEditor) return; +void MarkdownPreviewWidget::performUpdate() +{ + if (!m_sourceEditor) + return; + + // 刷新期间暂停轮询,避免在页面重新加载过程中误同步 + if (m_scrollSyncTimer) + m_scrollSyncTimer->stop(); const QString content = m_sourceEditor->toPlainText(); const QString html = generateHtml(content); + ++m_loadGeneration; m_webView->setHtml(html, QUrl("qrc:/")); - - // 延迟恢复滚动位置 - QTimer::singleShot(50, this, [this] { - m_webView->page()->runJavaScript( - QString("window.scrollTo(0, %1);").arg(m_lastScrollPosition) - ); - }); } -QString MarkdownPreviewWidget::generateHtml(const QString& markdown) { +QString MarkdownPreviewWidget::generateHtml(const QString& markdown) +{ // 使用 QJsonDocument 正确序列化字符串 QJsonArray a; a.append(QJsonValue(markdown)); @@ -223,6 +269,7 @@ QString MarkdownPreviewWidget::generateHtml(const QString& markdown) { %1 +
@@ -237,6 +284,19 @@ QString MarkdownPreviewWidget::generateHtml(const QString& markdown) { img.style.maxWidth = '100%'; img.style.height = 'auto'; }); + + // 通过 QWebChannel 将预览区滚动事件实时通知给 C++ 侧 + new QWebChannel(qt.webChannelTransport, function(channel) { + const bridge = channel.objects.markdownPreview; + if (!bridge) return; + window.addEventListener('scroll', function() { + const e = document.documentElement; + const h = e.scrollHeight - e.clientHeight; + if (h > 0) { + bridge.syncPreviewToEditor((window.scrollY || e.scrollTop || 0) / h); + } + }); + }); @@ -244,12 +304,27 @@ QString MarkdownPreviewWidget::generateHtml(const QString& markdown) { .arg(escapedMarkdown); // 直接插入已转义内容 } -void MarkdownPreviewWidget::setDarkTheme(bool enabled) { +void MarkdownPreviewWidget::setDarkTheme(bool enabled) +{ m_darkMode = enabled; scheduleUpdate(); } -void MarkdownPreviewWidget::initWebView() { +void MarkdownPreviewWidget::setPreviewScrollRatio(double ratio) +{ + if (!m_webView || !m_sourceEditor) + return; + + m_lastPreviewRatio = ratio; + m_webView->page()->runJavaScript( + QString("var e = document.documentElement;" + "var h = e.scrollHeight - e.clientHeight;" + "window.scrollTo(0, %1 * (h > 0 ? h : 0));").arg(ratio) + ); +} + +void MarkdownPreviewWidget::initWebView() +{ // 禁用上下文菜单 //m_webView->setContextMenuPolicy(Qt::NoContextMenu); diff --git a/src/widgets/markdownpreviewwidget.h b/src/widgets/markdownpreviewwidget.h index c795e91ef891294e379e9ae1e387e979aeae0179..c82537c187e9c2b88bd49e32ef2ce5fc6cd343dc 100644 --- a/src/widgets/markdownpreviewwidget.h +++ b/src/widgets/markdownpreviewwidget.h @@ -12,6 +12,22 @@ #include // 用于实现调用浏览器打开超链接 +class QWebChannel; +class MarkdownPreviewWidget; + +// 仅暴露滚动同步接口给 QWebChannel,避免把整个 QWidget 暴露给页面 +class MarkdownPreviewBridge : public QObject { + Q_OBJECT +public: + explicit MarkdownPreviewBridge(MarkdownPreviewWidget* widget); + +public slots: + void syncPreviewToEditor(double ratio); + +private: + MarkdownPreviewWidget* m_widget; +}; + class MarkdownWebPage : public QWebEnginePage { Q_OBJECT public: @@ -34,35 +50,35 @@ public: void setSourceEditor(QTextEdit* editor); void setDarkTheme(bool enabled); - static bool isSupport()/* { - // 检测是否有设置 QTWEBENGINE_DISABLE_SANDBOX - if (QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_DISABLE_SANDBOX") == "1") { - return true; - } - return QProcessEnvironment::systemEnvironment().value("USER") != "root"; - }*/; + static bool isSupport(); + + void syncPreviewToEditor(double ratio); private slots: void scheduleUpdate(); void performUpdate(); - void setupScrollSync(); void syncEditorToPreview(); + void pollPreviewScroll(); + void onPreviewLoadFinished(bool ok); private: void initWebView(); - void updateScrollPosition(); QString generateHtml(const QString& markdown); - double calculateSyncRatio(); + void setPreviewScrollRatio(double ratio); - QWebEngineView* m_webView; - MarkdownWebPage* m_webPage; + QWebEngineView* m_webView = nullptr; + MarkdownWebPage* m_webPage = nullptr; + QWebChannel* m_webChannel = nullptr; + MarkdownPreviewBridge* m_bridge = nullptr; QTextEdit* m_sourceEditor = nullptr; - QTimer* m_scrollSyncTimer; - QTimer* m_updateTimer; + QTimer* m_updateTimer = nullptr; + QTimer* m_scrollSyncTimer = nullptr; bool m_syncing = false; - int m_lastScrollPosition = 0; bool m_darkMode = false; + double m_lastScrollRatio = 0.0; + double m_lastPreviewRatio = 0.0; + int m_loadGeneration = 0; }; #endif #endif diff --git a/src/widgets/toast.cpp b/src/widgets/toast.cpp index fbb389becb9afccb8a0be4aa76436d5e1cc392b0..6f6e56c6bc2e693b24aabe719cf8ee5a55ad433c 100644 --- a/src/widgets/toast.cpp +++ b/src/widgets/toast.cpp @@ -19,6 +19,13 @@ #include "toast.h" #include +#include +#include + +static QString currentThemeName() +{ + return Dtk::Gui::DGuiApplicationHelper::instance()->themeType() == Dtk::Gui::DGuiApplicationHelper::DarkType ? "dark" : "light"; +} Toast::Toast(QWidget *parent) : QFrame(parent) @@ -49,8 +56,10 @@ Toast::Toast(QWidget *parent) setGraphicsEffect(m_effect); hide(); - setTheme(DThemeManager::instance()->theme()); - connect(DThemeManager::instance(), &DThemeManager::themeChanged, this, &Toast::setTheme); + setTheme(currentThemeName()); + connect(Dtk::Gui::DGuiApplicationHelper::instance(), &Dtk::Gui::DGuiApplicationHelper::themeTypeChanged, this, [this](Dtk::Gui::DGuiApplicationHelper::ColorType) { + setTheme(currentThemeName()); + }); connect(m_reloadBtn, &QPushButton::clicked, this, [=] { hideAnimation(); @@ -108,16 +117,18 @@ void Toast::setOnlyShow(bool onlyshow) m_onlyshow = onlyshow; if (m_onlyshow) { - m_closeBtn = new DImageButton; + m_closeBtn = new QPushButton; m_closeBtn->setFixedSize(14, 14); m_layout->addWidget(m_reloadBtn); m_layout->addWidget(m_saveAsBtn); m_layout->addWidget(m_closeBtn); - initCloseBtn(DThemeManager::instance()->theme()); + initCloseBtn(currentThemeName()); - connect(DThemeManager::instance(), &DThemeManager::themeChanged, this, &Toast::initCloseBtn); - connect(m_closeBtn, &DImageButton::clicked, this, &Toast::hideAnimation); - connect(m_closeBtn, &DImageButton::clicked, this, &Toast::closeBtnClicked); + connect(Dtk::Gui::DGuiApplicationHelper::instance(), &Dtk::Gui::DGuiApplicationHelper::themeTypeChanged, this, [this](Dtk::Gui::DGuiApplicationHelper::ColorType) { + initCloseBtn(currentThemeName()); + }); + connect(m_closeBtn, &QPushButton::clicked, this, &Toast::hideAnimation); + connect(m_closeBtn, &QPushButton::clicked, this, &Toast::closeBtnClicked); } } @@ -208,9 +219,8 @@ void Toast::setOpacity(qreal opacity) void Toast::initCloseBtn(QString theme) { - m_closeBtn->setNormalPic(QString(":/images/close_%1_%2.svg").arg("normal", theme)); - m_closeBtn->setHoverPic(QString(":/images/close_%1_%2.svg").arg("hover", theme)); - m_closeBtn->setPressPic(QString(":/images/close_%1_%2.svg").arg("press", theme)); + m_closeBtn->setIcon(QIcon(QString(":/images/close_%1_%2.svg").arg("normal", theme))); + m_closeBtn->setStyleSheet("QPushButton { border: none; }"); } void Toast::showEvent(QShowEvent *e) diff --git a/src/widgets/toast.h b/src/widgets/toast.h index 3912f15539d13d0a81675fac74a82e4c454de51f..e47a7d76ca3bc66e248125dd7522042bbf6229fd 100644 --- a/src/widgets/toast.h +++ b/src/widgets/toast.h @@ -28,8 +28,6 @@ #include #include -#include -#include #include #include @@ -82,7 +80,7 @@ private: QPropertyAnimation *m_animation = nullptr; DGraphicsGlowEffect *m_effect = nullptr; - DImageButton *m_closeBtn; + QPushButton *m_closeBtn; QPushButton *m_reloadBtn; QPushButton *m_saveAsBtn; }; diff --git a/src/window.cpp b/src/window.cpp index ab2b7c5803f8e82774be229380791721538156e1..1541fa440630986d73d4906ee0ffde2ac7a501c6 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -23,8 +23,8 @@ #include "window.h" #include "toolbar.h" #include -#include -#include +#include +#include #include "utils.h" #include @@ -166,8 +166,6 @@ Window::Window(DMainWindow *parent) connect(qApp, &QGuiApplication::focusWindowChanged, this, &Window::handleFocusWindowChanged); - // 允许设置背景 - setEnableWindowBackground(1); WaylandActivator::instance(); } @@ -209,7 +207,7 @@ void Window::initTitlebar() ToolBar *toolBar = new ToolBar; toolBar->setTabbar(m_tabbar); - titlebar()->setCustomWidget(toolBar, Qt::AlignVCenter, false); + titlebar()->setCustomWidget(toolBar, false); titlebar()->setAutoHideOnFullscreen(true); titlebar()->setSeparatorVisible(true); titlebar()->setFixedHeight(40); @@ -778,7 +776,6 @@ void Window::popupSettingsDialog() dialog->widgetFactory()->registerWidget("fontcombobox", Settings::createFontComBoBoxHandle); dialog->setProperty("_d_dtk_theme", "dark"); dialog->setProperty("_d_QSSFilename", "DSettingsDialog"); - DThemeManager::instance()->registerWidget(dialog); dialog->updateSettings(m_settings->settings); m_settings->dtkThemeWorkaround(dialog, "dlight"); @@ -1258,9 +1255,9 @@ void Window::loadTheme(const QString &path) bool isDark = (QColor(backgroundColor).lightness() < 128); if (isDark) { - DThemeManager::instance()->setTheme("dark"); + Dtk::Gui::DGuiApplicationHelper::instance()->setPaletteType(Dtk::Gui::DGuiApplicationHelper::DarkType); } else { - DThemeManager::instance()->setTheme("light"); + Dtk::Gui::DGuiApplicationHelper::instance()->setPaletteType(Dtk::Gui::DGuiApplicationHelper::LightType); } changeTitlebarBackground(tabbarStartColor, tabbarEndColor);