From 09cd9d664e3325130e8305797c2836d35f85a829 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Mon, 23 Sep 2024 10:59:03 -0400 Subject: [PATCH 01/12] Introduce QWebEngine Javascript Infrastructure Setup for TOC Javascript/CSS Injection --- kiwix-desktop.pro | 3 ++- resources/js.qrc | 5 +++++ resources/js/tableofcontent.js | 0 src/kprofile.cpp | 22 ++++++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 resources/js.qrc create mode 100644 resources/js/tableofcontent.js diff --git a/kiwix-desktop.pro b/kiwix-desktop.pro index 8d370af0..3a01ce94 100644 --- a/kiwix-desktop.pro +++ b/kiwix-desktop.pro @@ -224,6 +224,7 @@ RESOURCES += \ resources/translations.qrc \ resources/contentmanager.qrc \ resources/settingsmanager.qrc \ - resources/style.qrc + resources/style.qrc \ + resources/js.qrc RC_ICONS = resources/icons/kiwix/app_icon.ico diff --git a/resources/js.qrc b/resources/js.qrc new file mode 100644 index 00000000..39a8210a --- /dev/null +++ b/resources/js.qrc @@ -0,0 +1,5 @@ + + + js/tableofcontent.js + + diff --git a/resources/js/tableofcontent.js b/resources/js/tableofcontent.js new file mode 100644 index 00000000..e69de29b diff --git a/src/kprofile.cpp b/src/kprofile.cpp index 6659ebbd..252d2646 100644 --- a/src/kprofile.cpp +++ b/src/kprofile.cpp @@ -4,6 +4,26 @@ #include #include #include +#include +#include + +namespace +{ + +QWebEngineScript getScript(QString filename, + QWebEngineScript::InjectionPoint point = QWebEngineScript::DocumentReady) +{ + QWebEngineScript script; + script.setInjectionPoint(point); + script.setWorldId(QWebEngineScript::UserWorld); + + QFile scriptFile(filename); + scriptFile.open(QIODevice::ReadOnly); + script.setSourceCode(scriptFile.readAll()); + return script; +} + +} QString askForSaveFilePath(const QString& suggestedName) { @@ -36,6 +56,8 @@ KProfile::KProfile(QObject *parent) : #else // Qt 5.13 and later setUrlRequestInterceptor(new ExternalReqInterceptor(this)); #endif + + scripts()->insert(getScript(":/js/tableofcontent.js")); } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) From 2143e920b269afc2fa96ce64fc77f442bc0161fb Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Sun, 3 Nov 2024 16:29:08 -0500 Subject: [PATCH 02/12] Introduce KiwixWebChannelObject.{h, cpp} Add channel to communicate between web page and Qt --- kiwix-desktop.pro | 3 ++- resources/js/tableofcontent.js | 3 +++ src/kiwixwebchannelobject.h | 14 ++++++++++++++ src/kprofile.cpp | 2 ++ src/webview.cpp | 8 ++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/kiwixwebchannelobject.h diff --git a/kiwix-desktop.pro b/kiwix-desktop.pro index 3a01ce94..0eb8d9c0 100644 --- a/kiwix-desktop.pro +++ b/kiwix-desktop.pro @@ -5,7 +5,7 @@ #------------------------------------------------- QT += core gui network -QT += webenginewidgets +QT += webenginewidgets webchannel QT += printsupport # Avoid stripping incompatible files, due to false identification as executables, on WSL @@ -145,6 +145,7 @@ HEADERS += \ src/portutils.h \ src/css_constants.h \ src/multizimbutton.h \ + src/kiwixwebchannelobject.h \ FORMS += \ src/choiceitem.ui \ diff --git a/resources/js/tableofcontent.js b/resources/js/tableofcontent.js index e69de29b..ef9ca056 100644 --- a/resources/js/tableofcontent.js +++ b/resources/js/tableofcontent.js @@ -0,0 +1,3 @@ +new QWebChannel(qt.webChannelTransport, function(channel) { + var kiwixObj = channel.objects.kiwixChannelObj; +}); diff --git a/src/kiwixwebchannelobject.h b/src/kiwixwebchannelobject.h new file mode 100644 index 00000000..708178bf --- /dev/null +++ b/src/kiwixwebchannelobject.h @@ -0,0 +1,14 @@ +#ifndef KIWIXWEBCHANNELOBJECT_H +#define KIWIXWEBCHANNELOBJECT_H + +#include + +class KiwixWebChannelObject : public QObject +{ + Q_OBJECT + +public: + explicit KiwixWebChannelObject(QObject *parent = nullptr) : QObject(parent) {}; +}; + +#endif // KIWIXWEBCHANNELOBJECT_H diff --git a/src/kprofile.cpp b/src/kprofile.cpp index 252d2646..3aba83c2 100644 --- a/src/kprofile.cpp +++ b/src/kprofile.cpp @@ -58,6 +58,8 @@ KProfile::KProfile(QObject *parent) : #endif scripts()->insert(getScript(":/js/tableofcontent.js")); + scripts()->insert(getScript(":/qtwebchannel/qwebchannel.js", + QWebEngineScript::DocumentCreation)); } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) diff --git a/src/webview.cpp b/src/webview.cpp index 2004de7d..c740fa74 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -16,6 +16,9 @@ class QMenu; #include #include #include +#include +#include +#include "kiwixwebchannelobject.h" zim::Entry getArchiveEntryFromUrl(const zim::Archive& archive, const QUrl& url); QString askForSaveFilePath(const QString& suggestedName); @@ -97,6 +100,11 @@ WebView::WebView(QWidget *parent) } }); #endif + + const auto channel = new QWebChannel(this); + const auto kiwixChannelObj = new KiwixWebChannelObject; + page()->setWebChannel(channel, QWebEngineScript::UserWorld); + channel->registerObject("kiwixChannelObj", kiwixChannelObj); } WebView::~WebView() From 4620a3b01579f509edc2822f61a2610843974fc9 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Sun, 3 Nov 2024 17:52:28 -0500 Subject: [PATCH 03/12] Enter tableofcontentbar.{h,cpp,ui} Re-enabled ToggleTOCAction to display the bar. --- kiwix-desktop.pro | 5 +- src/kiwixapp.cpp | 5 +- src/mainwindow.cpp | 13 +++++ src/mainwindow.h | 1 + src/tableofcontentbar.cpp | 16 ++++++ src/tableofcontentbar.h | 22 ++++++++ src/tableofcontentbar.ui | 114 ++++++++++++++++++++++++++++++++++++++ src/topwidget.cpp | 3 + ui/mainwindow.ui | 7 +++ 9 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/tableofcontentbar.cpp create mode 100644 src/tableofcontentbar.h create mode 100644 src/tableofcontentbar.ui diff --git a/kiwix-desktop.pro b/kiwix-desktop.pro index 0eb8d9c0..296db7d4 100644 --- a/kiwix-desktop.pro +++ b/kiwix-desktop.pro @@ -85,6 +85,7 @@ SOURCES += \ src/tabbar.cpp \ src/contentmanagerside.cpp \ src/readinglistbar.cpp \ + src/tableofcontentbar.cpp \ src/klistwidgetitem.cpp \ src/opdsrequestmanager.cpp \ src/localkiwixserver.cpp \ @@ -135,6 +136,7 @@ HEADERS += \ src/tabbar.h \ src/contentmanagerside.h \ src/readinglistbar.h \ + src/tableofcontentbar.h \ src/klistwidgetitem.h \ src/opdsrequestmanager.h \ src/localkiwixserver.h \ @@ -158,7 +160,8 @@ FORMS += \ src/contentmanagerside.ui \ src/readinglistbar.ui \ ui/localkiwixserver.ui \ - ui/settings.ui + ui/settings.ui \ + src/tableofcontentbar.ui \ include(subprojects/QtSingleApplication/src/qtsingleapplication.pri) CODECFORSRC = UTF-8 diff --git a/src/kiwixapp.cpp b/src/kiwixapp.cpp index ed29b526..0404d6de 100644 --- a/src/kiwixapp.cpp +++ b/src/kiwixapp.cpp @@ -435,8 +435,8 @@ void KiwixApp::createActions() }); mpa_actions[ToggleFullscreenAction]->setCheckable(true); - CREATE_ACTION_SHORTCUT(ToggleTOCAction, gt("table-of-content"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_1)); - HIDE_ACTION(ToggleTOCAction); + CREATE_ACTION_ICON_SHORTCUT(ToggleTOCAction, "toc", gt("table-of-content"), QKeySequence(Qt::CTRL | Qt::Key_M)); + mpa_actions[ToggleTOCAction]->setCheckable(true); CREATE_ACTION_ICON_SHORTCUT(OpenMultiZimAction, "filter", gt("search-options"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_L)); @@ -492,6 +492,7 @@ void KiwixApp::handleItemsState(TabType tabType) auto libraryOrSettingsTab = (tabType == TabType::LibraryTab || tabType == TabType::SettingsTab); auto notBookmarkableTab = libraryOrSettingsTab || getTabWidget()->currentArticleUrl().isEmpty(); auto app = KiwixApp::instance(); + app->getAction(KiwixApp::ToggleTOCAction)->setDisabled(libraryOrSettingsTab); app->getAction(KiwixApp::ToggleReadingListAction)->setDisabled(libraryOrSettingsTab); app->getAction(KiwixApp::ToggleAddBookmarkAction)->setDisabled(notBookmarkableTab); app->getAction(KiwixApp::FindInPageAction)->setDisabled(libraryOrSettingsTab); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6b2feb85..20eb4683 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -45,6 +45,8 @@ MainWindow::MainWindow(QWidget *parent) : this, &MainWindow::toggleFullScreen); connect(app->getAction(KiwixApp::ToggleReadingListAction), &QAction::toggled, this, &MainWindow::readingListToggled); + connect(app->getAction(KiwixApp::ToggleTOCAction), &QAction::toggled, + this, &MainWindow::tableOfContentToggled); connect(app->getAction(KiwixApp::AboutAction), &QAction::triggered, mp_about, &QDialog::show); connect(app->getAction(KiwixApp::DonateAction), &QAction::triggered, @@ -181,6 +183,17 @@ void MainWindow::readingListToggled(bool state) } } +void MainWindow::tableOfContentToggled(bool state) +{ + if (state) { + mp_ui->sideBar->setCurrentWidget(mp_ui->tableofcontentbar); + mp_ui->sideBar->show(); + } + else { + mp_ui->sideBar->hide(); + } +} + void MainWindow::tabChanged(TabType tabType) { QAction *readingList = KiwixApp::instance()->getAction(KiwixApp::ToggleReadingListAction); diff --git a/src/mainwindow.h b/src/mainwindow.h index a97bef15..db44cdbc 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -35,6 +35,7 @@ private slots: void toggleFullScreen(); void tabChanged(TabBar::TabType); void readingListToggled(bool state); + void tableOfContentToggled(bool state); void hideTabAndTop(); void showTabAndTop(); void updateTabButtons(); diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp new file mode 100644 index 00000000..0864aa7e --- /dev/null +++ b/src/tableofcontentbar.cpp @@ -0,0 +1,16 @@ +#include "tableofcontentbar.h" +#include "ui_tableofcontentbar.h" +#include "kiwixapp.h" + +TableOfContentBar::TableOfContentBar(QWidget *parent) : + QFrame(parent), + ui(new Ui::tableofcontentbar) +{ + ui->setupUi(this); + ui->titleLabel->setText(gt("table-of-content")); +} + +TableOfContentBar::~TableOfContentBar() +{ + delete ui; +} diff --git a/src/tableofcontentbar.h b/src/tableofcontentbar.h new file mode 100644 index 00000000..53fdf8d2 --- /dev/null +++ b/src/tableofcontentbar.h @@ -0,0 +1,22 @@ +#ifndef TABLEOFCONTENTBAR_H +#define TABLEOFCONTENTBAR_H + +#include + +namespace Ui { +class tableofcontentbar; +} + +class TableOfContentBar : public QFrame +{ + Q_OBJECT + +public: + explicit TableOfContentBar(QWidget *parent = nullptr); + ~TableOfContentBar(); + +private: + Ui::tableofcontentbar *ui; +}; + +#endif // TABLEOFCONTENTBAR_H diff --git a/src/tableofcontentbar.ui b/src/tableofcontentbar.ui new file mode 100644 index 00000000..8c793d8e --- /dev/null +++ b/src/tableofcontentbar.ui @@ -0,0 +1,114 @@ + + + tableofcontentbar + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 16 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + QFrame::NoFrame + + + 0 + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + Qt::ElideNone + + + 30 + + + true + + + true + + + true + + + false + + + + 1 + + + + + + + + + diff --git a/src/topwidget.cpp b/src/topwidget.cpp index c29d5927..9843b0bf 100644 --- a/src/topwidget.cpp +++ b/src/topwidget.cpp @@ -30,6 +30,9 @@ TopWidget::TopWidget(QWidget *parent) : QAction *random = app->getAction(KiwixApp::RandomArticleAction); addAction(random); + QAction *toc = app->getAction(KiwixApp::ToggleTOCAction); + addAction(toc); + // For CSS if (QGuiApplication::isLeftToRight()) { widgetForAction(back)->setObjectName("leftHistoryButton"); diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index dd2749e3..269b91bf 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -145,6 +145,7 @@ + @@ -201,6 +202,12 @@
src/readinglistbar.h
1 + + TableOfContentBar + QWidget +
src/tableofcontentbar.h
+ 1 +
From 5f746216fae10a6a98e6f4a196170c81c5d68ee6 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Sun, 3 Nov 2024 18:16:34 -0500 Subject: [PATCH 04/12] Ensure ReadingList&TOC display states are consistency Only one should be in checked state. --- src/mainwindow.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 20eb4683..3a57c9c4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -172,9 +172,18 @@ void MainWindow::resizeEvent(QResizeEvent *event) updateTabButtons(); } +void checkActionNoSignal(KiwixApp::Actions actionFlag, bool checked) +{ + const auto action = KiwixApp::instance()->getAction(actionFlag); + const bool oldState = action->blockSignals(true); + action->setChecked(checked); + action->blockSignals(oldState); +} + void MainWindow::readingListToggled(bool state) { if (state) { + checkActionNoSignal(KiwixApp::ToggleTOCAction, false); mp_ui->sideBar->setCurrentWidget(mp_ui->readinglistbar); mp_ui->sideBar->show(); } @@ -186,6 +195,7 @@ void MainWindow::readingListToggled(bool state) void MainWindow::tableOfContentToggled(bool state) { if (state) { + checkActionNoSignal(KiwixApp::ToggleReadingListAction, false); mp_ui->sideBar->setCurrentWidget(mp_ui->tableofcontentbar); mp_ui->sideBar->show(); } @@ -197,13 +207,18 @@ void MainWindow::tableOfContentToggled(bool state) void MainWindow::tabChanged(TabType tabType) { QAction *readingList = KiwixApp::instance()->getAction(KiwixApp::ToggleReadingListAction); + QAction *tableOfContent = KiwixApp::instance()->getAction(KiwixApp::ToggleTOCAction); if (tabType == TabType::SettingsTab) { mp_ui->sideBar->hide(); } else if(tabType == TabType::LibraryTab) { mp_ui->sideBar->setCurrentWidget(mp_ui->contentmanagerside); mp_ui->sideBar->show(); - } else { - readingListToggled(readingList->isChecked()); + } else if (readingList->isChecked()) { + readingListToggled(true); + } else if (tableOfContent->isChecked()) { + tableOfContentToggled(true); + } else { + mp_ui->sideBar->hide(); } } From a60f676d51cdce72327f288a31e158708f79b1a4 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Mon, 4 Nov 2024 02:17:12 -0500 Subject: [PATCH 05/12] Signal Current TOC Parsed from JS Signal emits the collected header JSON. Parsing delegated to JS due to better DOM manipulations. --- resources/js/tableofcontent.js | 72 ++++++++++++++++++++++++++++++++++ src/kiwixwebchannelobject.h | 5 +++ src/webview.cpp | 30 +++++++++++++- src/webview.h | 5 +++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/resources/js/tableofcontent.js b/resources/js/tableofcontent.js index ef9ca056..8ada9c6e 100644 --- a/resources/js/tableofcontent.js +++ b/resources/js/tableofcontent.js @@ -1,3 +1,75 @@ +/** + * Construct recurseData.str as a JSON Array that contains their header text, + * link, and children headers. + * + * References: + * https://stackoverflow.com/questions/187619/is-there-a-javascript-solution-to-generating-a-table-of-contents-for-a-page + * @param elem DOM element + * @param recurseData Object with fields: { level : int, toc : str, count : int, Set : levelSet } + */ +function recurseChild(elem, recurseData) +{ + if (elem !== "undefined") + { + if(elem.nodeName.match(/^H\d+$/) && elem.textContent) + { + var headerText = elem.textContent.trim(); + var prevLevel = recurseData.level; + var level = elem.nodeName.substr(1); + var anchor = "kiwix-toc-" + recurseData.count; + var anchorLink = window.location.href.replace(location.hash,"") + '#' + anchor; + recurseData.count += 1; + + var anchorElem = document.createElement("a"); + anchorElem.id = anchor; + + /* Wrap header content with something we can reference. */ + elem.insertAdjacentElement("afterbegin", anchorElem); + + if (level < prevLevel) + { + /* Complete current element and the parent element.*/ + recurseData.toc += ']}]}, '; + } + else if (level == prevLevel) + { + /* Complete current element*/ + recurseData.toc += ']}, '; + } + + recurseData.level = parseInt(level); + recurseData.levelSet.add(parseInt(level)); + recurseData.toc += '{"text" : "' + headerText.replace(/"/g, '\\"') + '", "anchor": "' + anchorLink + '", ' + '"child" : ['; + } + + var c = elem.children; + for (var i = 0; i < c.length; i++) + recurseChild(c[i], recurseData); + } +} + +function tocJSON() +{ + /* level used to track current header level. + toc used to store constructed list. + count used to uniquely identify each list item in toc. + levelSet used to retrieve the levels disregarding header level value. + */ + var recurseData = { level: 0, toc: '{ "url" : "' + window.location.href.replace(location.hash,"") + '", "table" : [', count: 0, levelSet: new Set}; + recurseChild(document.body, recurseData); + + var levelArray = Array.from(recurseData.levelSet).sort(); + levelArray.sort(function(a, b){return a - b}); + var level = levelArray.indexOf(recurseData.level) + 1; + + /* End un-closed lists */ + if (level) + recurseData.toc += (new Array(level + 1)).join(']}'); + recurseData.toc += ']}'; + return recurseData.toc; +} + new QWebChannel(qt.webChannelTransport, function(channel) { var kiwixObj = channel.objects.kiwixChannelObj; + kiwixObj.sendTableOfContent(tocJSON()); }); diff --git a/src/kiwixwebchannelobject.h b/src/kiwixwebchannelobject.h index 708178bf..f14e94d0 100644 --- a/src/kiwixwebchannelobject.h +++ b/src/kiwixwebchannelobject.h @@ -9,6 +9,11 @@ class KiwixWebChannelObject : public QObject public: explicit KiwixWebChannelObject(QObject *parent = nullptr) : QObject(parent) {}; + + Q_INVOKABLE void sendTableOfContent(const QString& tableJson) { emit tableOfContentChanged(tableJson); }; + +signals: + void tableOfContentChanged(const QString& tableJson); }; #endif // KIWIXWEBCHANNELOBJECT_H diff --git a/src/webview.cpp b/src/webview.cpp index c740fa74..cee0c1c7 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -105,6 +105,10 @@ WebView::WebView(QWidget *parent) const auto kiwixChannelObj = new KiwixWebChannelObject; page()->setWebChannel(channel, QWebEngineScript::UserWorld); channel->registerObject("kiwixChannelObj", kiwixChannelObj); + + const auto tabbar = KiwixApp::instance()->getTabWidget(); + connect(tabbar, &TabBar::currentTitleChanged, this, &WebView::onCurrentTitleChanged); + connect(kiwixChannelObj, &KiwixWebChannelObject::tableOfContentChanged, this, &WebView::onTableOfContentRecieved); } WebView::~WebView() @@ -198,7 +202,31 @@ void WebView::saveViewContent() catch (...) { /* Blank */} } -void WebView::addHistoryItemAction(QMenu *menu, const QWebEngineHistoryItem &item, int n) const +void WebView::onCurrentTitleChanged() +{ + const auto tabbar = KiwixApp::instance()->getTabWidget(); + const auto tableObject = m_tableOfContent.object(); + const auto tableValid = tableObject["url"].toString() == url().url(); + + /* When table invalid, then we are loading and the emit will be + handled by KiwixWebChannelObject::tableOfContentChanged. + */ + if (tabbar->currentWebView() == this && tableValid) + emit tableOfContentChanged(m_tableOfContent); +} + +void WebView::onTableOfContentRecieved(const QString& tableJson) +{ + const auto tabbar = KiwixApp::instance()->getTabWidget(); + m_tableOfContent = QJsonDocument::fromJson(tableJson.toUtf8()); + + if (tabbar->currentWebView() == this) + emit tableOfContentChanged(m_tableOfContent); +} + +void WebView::addHistoryItemAction(QMenu *menu, + const QWebEngineHistoryItem &item, + int n) const { QAction *a = menu->addAction(item.title()); a->setData(QVariant::fromValue(n)); diff --git a/src/webview.h b/src/webview.h index e2419946..4f53e4e8 100644 --- a/src/webview.h +++ b/src/webview.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "findinpagebar.h" @@ -53,6 +54,7 @@ public slots: signals: void iconChanged(const QIcon& icon); void zimIdChanged(const QString& zimId); + void tableOfContentChanged(const QJsonDocument& table); protected: virtual QWebEngineView* createWindow(QWebEnginePage::WebWindowType type); @@ -67,12 +69,15 @@ public slots: private slots: void gotoTriggeredHistoryItemAction(); + void onCurrentTitleChanged(); + void onTableOfContentRecieved(const QString& tableJson); private: void addHistoryItemAction(QMenu *menu, const QWebEngineHistoryItem &item, int n) const; void applyCorrectZoomFactor(); QMenu* createStandardContextMenu(); QMenu* createLinkContextMenu(); + QJsonDocument m_tableOfContent; }; #endif // WEBVIEW_H From 8e45c8536b256318b8287c59e68d32b87d7c0e50 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Wed, 6 Nov 2024 13:34:45 -0500 Subject: [PATCH 06/12] Connect Sidebar to Display Table Of Content Recursively load the header JSON. --- src/mainwindow.cpp | 5 +++++ src/mainwindow.h | 3 +++ src/tableofcontentbar.cpp | 32 ++++++++++++++++++++++++++++++++ src/tableofcontentbar.h | 3 +++ src/webview.cpp | 4 ++++ 5 files changed, 47 insertions(+) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3a57c9c4..a298e9c2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -231,3 +231,8 @@ TopWidget *MainWindow::getTopWidget() { return mp_ui->mainToolBar; } + +TableOfContentBar *MainWindow::getTableOfContentBar() +{ + return mp_ui->tableofcontentbar; +} diff --git a/src/mainwindow.h b/src/mainwindow.h index db44cdbc..4e580d1a 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -14,6 +14,8 @@ namespace Ui { class MainWindow; } +class TableOfContentBar; + class MainWindow : public QMainWindow { Q_OBJECT @@ -25,6 +27,7 @@ class MainWindow : public QMainWindow TabBar* getTabBar(); TopWidget* getTopWidget(); QWidget getMainView(); + TableOfContentBar *getTableOfContentBar(); protected: bool eventFilter(QObject* object, QEvent* event) override; diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index 0864aa7e..4b2e1aa4 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -1,6 +1,8 @@ #include "tableofcontentbar.h" #include "ui_tableofcontentbar.h" #include "kiwixapp.h" +#include +#include TableOfContentBar::TableOfContentBar(QWidget *parent) : QFrame(parent), @@ -8,9 +10,39 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) : { ui->setupUi(this); ui->titleLabel->setText(gt("table-of-content")); + ui->tree->setRootIsDecorated(false); } TableOfContentBar::~TableOfContentBar() { delete ui; } + +namespace +{ + +void populateItem(const QJsonArray& headerArray, QTreeWidgetItem* parent) +{ + for (int i = 0; i < headerArray.size(); i++) + { + const auto header = headerArray[i].toObject(); + const auto item = new QTreeWidgetItem(parent); + item->setExpanded(true); + item->setData(0, Qt::DisplayRole, header["text"].toString()); + populateItem(header["child"].toArray(), item); + } +} + +} + +void TableOfContentBar::setupTree(const QJsonDocument& table) +{ + const auto tableUrl = table["url"].toString(); + const auto webView = KiwixApp::instance()->getTabWidget()->currentWebView(); + const auto currentUrl = webView->url().url(QUrl::RemoveFragment); + if (tableUrl != currentUrl) + return; + + ui->tree->clear(); + populateItem(table["table"].toArray(), ui->tree->invisibleRootItem()); +} diff --git a/src/tableofcontentbar.h b/src/tableofcontentbar.h index 53fdf8d2..64a8ba2d 100644 --- a/src/tableofcontentbar.h +++ b/src/tableofcontentbar.h @@ -15,6 +15,9 @@ class TableOfContentBar : public QFrame explicit TableOfContentBar(QWidget *parent = nullptr); ~TableOfContentBar(); +public slots: + void setupTree(const QJsonDocument& table); + private: Ui::tableofcontentbar *ui; }; diff --git a/src/webview.cpp b/src/webview.cpp index cee0c1c7..62f383be 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -19,6 +19,7 @@ class QMenu; #include #include #include "kiwixwebchannelobject.h" +#include "tableofcontentbar.h" zim::Entry getArchiveEntryFromUrl(const zim::Archive& archive, const QUrl& url); QString askForSaveFilePath(const QString& suggestedName); @@ -109,6 +110,9 @@ WebView::WebView(QWidget *parent) const auto tabbar = KiwixApp::instance()->getTabWidget(); connect(tabbar, &TabBar::currentTitleChanged, this, &WebView::onCurrentTitleChanged); connect(kiwixChannelObj, &KiwixWebChannelObject::tableOfContentChanged, this, &WebView::onTableOfContentRecieved); + + const auto tocbar = KiwixApp::instance()->getMainWindow()->getTableOfContentBar(); + connect(this, &WebView::tableOfContentChanged, tocbar, &TableOfContentBar::setupTree); } WebView::~WebView() From 77a5e566a5e2bc35bbdcc52e49800d2664f1ae9f Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Wed, 6 Nov 2024 18:48:26 -0500 Subject: [PATCH 07/12] Navigate to Header On Click in TOC Upon signaling, use JS to navigate, as helps avoid wrong url setting given the asynchronism. --- src/tableofcontentbar.cpp | 4 ++++ src/tableofcontentbar.h | 3 +++ src/webview.cpp | 18 ++++++++++++++++++ src/webview.h | 1 + 4 files changed, 26 insertions(+) diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index 4b2e1aa4..e0d7e913 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -11,6 +11,9 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) : ui->setupUi(this); ui->titleLabel->setText(gt("table-of-content")); ui->tree->setRootIsDecorated(false); + connect(ui->tree, &QTreeWidget::itemActivated, this, [=](QTreeWidgetItem* item) { + emit navigationRequested(item->data(0, Qt::UserRole).toString()); + }); } TableOfContentBar::~TableOfContentBar() @@ -29,6 +32,7 @@ void populateItem(const QJsonArray& headerArray, QTreeWidgetItem* parent) const auto item = new QTreeWidgetItem(parent); item->setExpanded(true); item->setData(0, Qt::DisplayRole, header["text"].toString()); + item->setData(0, Qt::UserRole, header["anchor"].toString()); populateItem(header["child"].toArray(), item); } } diff --git a/src/tableofcontentbar.h b/src/tableofcontentbar.h index 64a8ba2d..0566ad7f 100644 --- a/src/tableofcontentbar.h +++ b/src/tableofcontentbar.h @@ -18,6 +18,9 @@ class TableOfContentBar : public QFrame public slots: void setupTree(const QJsonDocument& table); +signals: + void navigationRequested(const QUrl& anchor); + private: Ui::tableofcontentbar *ui; }; diff --git a/src/webview.cpp b/src/webview.cpp index 62f383be..70420795 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -113,6 +113,7 @@ WebView::WebView(QWidget *parent) const auto tocbar = KiwixApp::instance()->getMainWindow()->getTableOfContentBar(); connect(this, &WebView::tableOfContentChanged, tocbar, &TableOfContentBar::setupTree); + connect(tocbar, &TableOfContentBar::navigationRequested, this, &WebView::onNavigationRequested); } WebView::~WebView() @@ -228,6 +229,23 @@ void WebView::onTableOfContentRecieved(const QString& tableJson) emit tableOfContentChanged(m_tableOfContent); } +void WebView::onNavigationRequested(const QUrl &anchor) +{ + const auto tabbar = KiwixApp::instance()->getTabWidget(); + const auto currentUrl = url().url(); + const auto anchorNoHash = anchor.url(QUrl::RemoveFragment); + + if (tabbar->currentWebView() == this && anchorNoHash == currentUrl) + { + setUrl(anchor); + + /* We need to reset the url, otherwise repeated navigations + and url checks from the TableOfContentBar would fail. + */ + setUrl(currentUrl); + } +} + void WebView::addHistoryItemAction(QMenu *menu, const QWebEngineHistoryItem &item, int n) const diff --git a/src/webview.h b/src/webview.h index 4f53e4e8..8045fc82 100644 --- a/src/webview.h +++ b/src/webview.h @@ -71,6 +71,7 @@ private slots: void gotoTriggeredHistoryItemAction(); void onCurrentTitleChanged(); void onTableOfContentRecieved(const QString& tableJson); + void onNavigationRequested(const QUrl& anchor); private: void addHistoryItemAction(QMenu *menu, const QWebEngineHistoryItem &item, int n) const; From bdfbffaad8b4e1c2090c2dadd2b60d8abfdd7b37 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Wed, 6 Nov 2024 19:41:02 -0500 Subject: [PATCH 08/12] Number TOC Header Entries --- src/tableofcontentbar.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index e0d7e913..2e6e13d4 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -31,7 +31,14 @@ void populateItem(const QJsonArray& headerArray, QTreeWidgetItem* parent) const auto header = headerArray[i].toObject(); const auto item = new QTreeWidgetItem(parent); item->setExpanded(true); - item->setData(0, Qt::DisplayRole, header["text"].toString()); + + auto numberList = parent->data(0, Qt::UserRole + 1).toStringList(); + numberList.append(QString::number(i + 1)); + item->setData(0, Qt::UserRole + 1, numberList); + + const auto itemNum = numberList.join("."); + const auto display = itemNum + " " + header["text"].toString(); + item->setData(0, Qt::DisplayRole, display); item->setData(0, Qt::UserRole, header["anchor"].toString()); populateItem(header["child"].toArray(), item); } @@ -48,5 +55,6 @@ void TableOfContentBar::setupTree(const QJsonDocument& table) return; ui->tree->clear(); + ui->tree->invisibleRootItem()->setData(0, Qt::UserRole + 1, QStringList{}); populateItem(table["table"].toArray(), ui->tree->invisibleRootItem()); } From 3a075181e027e7a193232d6ca0d3892ef57dbbcc Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Wed, 6 Nov 2024 20:02:27 -0500 Subject: [PATCH 09/12] Add Hide Button to TOC --- src/tableofcontentbar.cpp | 8 ++++++++ src/tableofcontentbar.ui | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index 2e6e13d4..b91434fe 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -10,6 +10,14 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) : { ui->setupUi(this); ui->titleLabel->setText(gt("table-of-content")); + ui->hideLabel->setTextFormat(Qt::RichText); + + /* href is needed to make hide clickable, but not used. So Kiwix it is :) */ + ui->hideLabel->setText("" + gt("hide") + ""); + connect(ui->hideLabel, &QLabel::linkActivated, this, [=](){ + KiwixApp::instance()->getAction(KiwixApp::ToggleTOCAction)->setChecked(false); + }); + ui->tree->setRootIsDecorated(false); connect(ui->tree, &QTreeWidget::itemActivated, this, [=](QTreeWidgetItem* item) { emit navigationRequested(item->data(0, Qt::UserRole).toString()); diff --git a/src/tableofcontentbar.ui b/src/tableofcontentbar.ui index 8c793d8e..987a889c 100644 --- a/src/tableofcontentbar.ui +++ b/src/tableofcontentbar.ui @@ -59,6 +59,13 @@ + + + + + + + From df59543a6e47e75047ef82562fe4563c3a967e87 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Thu, 7 Nov 2024 02:28:58 -0500 Subject: [PATCH 10/12] Proper Font and Sizes for TOC --- resources/css/style.css | 4 ++++ src/tableofcontentbar.cpp | 3 +++ 2 files changed, 7 insertions(+) diff --git a/resources/css/style.css b/resources/css/style.css index e3b08c85..1e56d27e 100644 --- a/resources/css/style.css +++ b/resources/css/style.css @@ -400,3 +400,7 @@ ContentTypeFilter { width: 0; height: 0; } + +#tableofcontentbar QTreeWidget::item { + height: 26px; +} diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index b91434fe..1e95f8f4 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -9,7 +9,9 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) : ui(new Ui::tableofcontentbar) { ui->setupUi(this); + ui->titleLabel->setFont(QFont("Selawik", 18, QFont::Weight::Medium)); ui->titleLabel->setText(gt("table-of-content")); + ui->hideLabel->setFont(QFont("Selawik", 12)); ui->hideLabel->setTextFormat(Qt::RichText); /* href is needed to make hide clickable, but not used. So Kiwix it is :) */ @@ -47,6 +49,7 @@ void populateItem(const QJsonArray& headerArray, QTreeWidgetItem* parent) const auto itemNum = numberList.join("."); const auto display = itemNum + " " + header["text"].toString(); item->setData(0, Qt::DisplayRole, display); + item->setData(0, Qt::FontRole, QFont("Selawik", 12)); item->setData(0, Qt::UserRole, header["anchor"].toString()); populateItem(header["child"].toArray(), item); } From f389411f8732d5a1fa4330ef56a6d59293907005 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Thu, 7 Nov 2024 02:34:29 -0500 Subject: [PATCH 11/12] Proper Styling to TOC --- resources/css/style.css | 64 ++++++++++++++++++++++++++++++++++++++++ src/tableofcontentbar.ui | 11 ++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/resources/css/style.css b/resources/css/style.css index 1e56d27e..c0b10a68 100644 --- a/resources/css/style.css +++ b/resources/css/style.css @@ -401,6 +401,70 @@ ContentTypeFilter { height: 0; } +#tableofcontentbar { + background-color: white; +} + +#tableofcontentbar QTreeWidget, +#tableofcontentbar QLabel, +#tableofcontentbar QFrame { + background-color: white; +} + +#tableofcontentbar QTreeWidget { + outline: none; +} + #tableofcontentbar QTreeWidget::item { height: 26px; + padding: 0px 10px; + outline: none; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +#tableofcontentbar QTreeWidget::item:selected, +#tableofcontentbar QTreeWidget::item:hover { + outline: none; + border-top: 1px solid #3366CC; + border-bottom: 1px solid #3366CC; + background-color: #D9E9FF; + color: black; +} + +#tableofcontentbar QTreeWidget::branch:selected, +#tableofcontentbar QTreeWidget::branch:hover { + outline: none; + border-top: 1px solid #3366CC; + border-bottom: 1px solid #3366CC; + background-color: #D9E9FF; +} + +#tableofcontentbar QTreeWidget::branch:has-children:closed { + padding: 5px; /* Can only change icon size with padding. */ + image: url(:/icons/caret-up-solid.svg); +} + +#tableofcontentbar QTreeWidget::branch:has-children { + padding: 5px; /* Can only change icon size with padding. */ + image: url(:/icons/caret-down-solid.svg); +} + +#tableofcontentbar #titleLabel { + padding: 0px; + margin: 10px; +} + +#tableofcontentbar #hideLabel { + margin: 13px 10px 10px; /* 3px to match bottom with titleLabel */ +} + +#tableofcontentbar QScrollBar { + width: 5px; + border: none; + outline: none; +} + +#tableofcontentbar QScrollBar::handle { + background-color: grey; } diff --git a/src/tableofcontentbar.ui b/src/tableofcontentbar.ui index 987a889c..8ebf8750 100644 --- a/src/tableofcontentbar.ui +++ b/src/tableofcontentbar.ui @@ -20,6 +20,9 @@ Form + + 0 + 0 @@ -44,6 +47,9 @@ + + 0 +
@@ -64,6 +70,9 @@ + + 0 + @@ -90,7 +99,7 @@ QAbstractScrollArea::AdjustToContents - Qt::ElideNone + Qt::ElideRight 30 From 6d4aca1786ad21e8781791b6c266d9b115d18ac9 Mon Sep 17 00:00:00 2001 From: ShaopengLin Date: Thu, 7 Nov 2024 02:34:55 -0500 Subject: [PATCH 12/12] Add Tooltip to TOC Elided item text is expanded here. --- src/tableofcontentbar.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tableofcontentbar.cpp b/src/tableofcontentbar.cpp index 1e95f8f4..f3252dc8 100644 --- a/src/tableofcontentbar.cpp +++ b/src/tableofcontentbar.cpp @@ -51,6 +51,7 @@ void populateItem(const QJsonArray& headerArray, QTreeWidgetItem* parent) item->setData(0, Qt::DisplayRole, display); item->setData(0, Qt::FontRole, QFont("Selawik", 12)); item->setData(0, Qt::UserRole, header["anchor"].toString()); + item->setToolTip(0, display); populateItem(header["child"].toArray(), item); } }