Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Table of Content Without Intense JS Invasion #1237

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
11 changes: 8 additions & 3 deletions kiwix-desktop.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand All @@ -145,6 +147,7 @@ HEADERS += \
src/portutils.h \
src/css_constants.h \
src/multizimbutton.h \
src/kiwixwebchannelobject.h \

FORMS += \
src/choiceitem.ui \
Expand All @@ -157,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
Expand Down Expand Up @@ -224,6 +228,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
68 changes: 68 additions & 0 deletions resources/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,71 @@ ContentTypeFilter {
width: 0;
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;
}
5 changes: 5 additions & 0 deletions resources/js.qrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>js/tableofcontent.js</file>
</qresource>
</RCC>
75 changes: 75 additions & 0 deletions resources/js/tableofcontent.js
Original file line number Diff line number Diff line change
@@ -0,0 +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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that it doesn't make sense to include the full URL with every link. The URL of the page can be provided only once at the root level of the returned JSON object while each TOC entry will include only the 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;
}
Comment on lines +10 to +70
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that instead of constructing the final JSON string it is better to construct a recursive JS object and then call JSON.stringify() on it.


new QWebChannel(qt.webChannelTransport, function(channel) {
var kiwixObj = channel.objects.kiwixChannelObj;
kiwixObj.sendTableOfContent(tocJSON());
});
5 changes: 3 additions & 2 deletions src/kiwixapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/kiwixwebchannelobject.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef KIWIXWEBCHANNELOBJECT_H
#define KIWIXWEBCHANNELOBJECT_H

#include <QObject>

class KiwixWebChannelObject : public QObject
{
Q_OBJECT

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
24 changes: 24 additions & 0 deletions src/kprofile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QWebEngineSettings>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>

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;
}
Comment on lines +13 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hide in unnamed namespace


}

QString askForSaveFilePath(const QString& suggestedName)
{
Expand Down Expand Up @@ -36,6 +56,10 @@ KProfile::KProfile(QObject *parent) :
#else // Qt 5.13 and later
setUrlRequestInterceptor(new ExternalReqInterceptor(this));
#endif

scripts()->insert(getScript(":/js/tableofcontent.js"));
scripts()->insert(getScript(":/qtwebchannel/qwebchannel.js",
QWebEngineScript::DocumentCreation));
}

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
Expand Down
37 changes: 35 additions & 2 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -170,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();
}
Expand All @@ -181,16 +192,33 @@ 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();
}
else {
mp_ui->sideBar->hide();
}
}

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();
}
}

Expand All @@ -203,3 +231,8 @@ TopWidget *MainWindow::getTopWidget()
{
return mp_ui->mainToolBar;
}

TableOfContentBar *MainWindow::getTableOfContentBar()
{
return mp_ui->tableofcontentbar;
}
4 changes: 4 additions & 0 deletions src/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Ui {
class MainWindow;
}

class TableOfContentBar;

class MainWindow : public QMainWindow
{
Q_OBJECT
Expand All @@ -25,6 +27,7 @@ class MainWindow : public QMainWindow
TabBar* getTabBar();
TopWidget* getTopWidget();
QWidget getMainView();
TableOfContentBar *getTableOfContentBar();

protected:
bool eventFilter(QObject* object, QEvent* event) override;
Expand All @@ -35,6 +38,7 @@ private slots:
void toggleFullScreen();
void tabChanged(TabBar::TabType);
void readingListToggled(bool state);
void tableOfContentToggled(bool state);
void hideTabAndTop();
void showTabAndTop();
void updateTabButtons();
Expand Down
Loading
Loading