diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-06 14:22:53 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-06 14:22:53 +0200 |
| commit | bcda97a2e17f6e1366cfe5b03bd0b407d4484255 (patch) | |
| tree | 2f485e8ff94e26a4c8e1b2e0310c396dbb23c5a0 | |
| parent | 7e7ac47308d88aa3a67836937a6888b7b3d90d56 (diff) | |
| download | kristall-bcda97a2e17f6e1366cfe5b03bd0b407d4484255.tar.gz | |
Reworks document rendering: Now generates QTextDocument directly instead of using HTML inbetween.
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | browsertab.cpp | 279 | ||||
| -rw-r--r-- | browsertab.hpp | 21 | ||||
| -rw-r--r-- | browsertab.ui | 14 | ||||
| -rw-r--r-- | documentoutlinemodel.cpp | 9 | ||||
| -rw-r--r-- | documentoutlinemodel.hpp | 2 | ||||
| -rw-r--r-- | icons.qrc | 1 | ||||
| -rw-r--r-- | icons/close.svg | 1 | ||||
| -rw-r--r-- | mainwindow.cpp | 22 | ||||
| -rw-r--r-- | mainwindow.hpp | 5 |
10 files changed, 252 insertions, 119 deletions
@@ -1,10 +1,25 @@ # Kristall A high-quality visual cross-platform gemini browser. + + +## Features +- Document rendering + - `text/gemini` + - `text/html` (reduced feature set) + - `text/markdown` + - `text/*` + - `image/* +- Outline generation +- Favourite Sites +- Tabbed interface +- Survives [ConMans torture suite](gemini://gemini.conman.org/test/torture/) + ## TODO - [ ] Survive full torture suite - [ ] Correctly parse mime parameters - [ ] Correctly parse charset (0013, 0014) - [ ] Correctly parse other params (0015) - [ ] Correctly parse undefined params (0016) - - [ ]
\ No newline at end of file +- [ ] Make document style customizable +- [ ] Add history navigation
\ No newline at end of file diff --git a/browsertab.cpp b/browsertab.cpp index 005926d..153e19f 100644 --- a/browsertab.cpp +++ b/browsertab.cpp @@ -9,6 +9,8 @@ #include <QDockWidget> #include <QImage> #include <QPixmap> +#include <QTextList> +#include <QTextBlock> #include <QGraphicsPixmapItem> #include <QGraphicsTextItem> @@ -34,28 +36,11 @@ BrowserTab::BrowserTab(MainWindow * mainWindow) : this->updateUI(); + this->ui->graphics_browser->setVisible(false); + this->ui->text_browser->setVisible(false); + this->ui->graphics_browser->setScene(&graphics_scene); - this->ui->text_browser->document()->setDocumentMargin(55.0); - this->ui->text_browser->document()->setDefaultStyleSheet( - R"css( - h1 { - color: red; - } - h2 { - color: green; - } - h3 { - color: gold; - } - a { - color: blue; - } - ul { - -qt-list-indent: 1; - type: square; - } - )css"); } BrowserTab::~BrowserTab() @@ -119,27 +104,6 @@ void BrowserTab::on_url_bar_returnPressed() this->navigateTo(this->ui->url_bar->text()); } -void BrowserTab::on_content_titleChanged(const QString &title) -{ - this->setWindowTitle(title); -} - -void BrowserTab::on_content_loadStarted() -{ - this->ui->refresh_button->setEnabled(false); -} - -void BrowserTab::on_content_loadFinished(bool ok) -{ - this->ui->refresh_button->setEnabled(true); -} - -void BrowserTab::on_content_urlChanged(const QUrl &url) -{ - // qDebug() << "url changed to" << url; - // this->ui->url_bar->setText(url.toString()); -} - void BrowserTab::on_refresh_button_clicked() { if(current_location.isValid()) @@ -157,32 +121,42 @@ void BrowserTab::on_gemini_complete(const QByteArray &data, const QString &mime) this->ui->text_browser->setVisible(mime.startsWith("text/")); this->ui->graphics_browser->setVisible(mime.startsWith("image/")); + std::unique_ptr<QTextDocument> document; + + this->outline.clear(); + if(mime.startsWith("text/gemini")) { - auto html = translateGeminiToHtml(data, this->outline); - this->ui->text_browser->setHtml(html); + document = translateGemini(data, this->current_location, this->outline); } else if(mime.startsWith("text/html")) { - this->ui->text_browser->setHtml(QString::fromUtf8(data)); + document = std::make_unique<QTextDocument>(); + document->setHtml(QString::fromUtf8(data)); } #if QT_CONFIG(textmarkdownreader) else if(mime.startsWith("text/markdown")) { - this->ui->text_browser->setMarkdown(QString::fromUtf8(data)); + document = std::make_unique<QTextDocument>(); + document->setMarkdown(QString::fromUtf8(data)); } #endif else if(mime.startsWith("text/")) { - this->ui->text_browser->setPlainText(QString::fromUtf8(data)); + QFont monospace; + monospace.setFamily("monospace"); + + document = std::make_unique<QTextDocument>(); + document->setDefaultFont(monospace); + document->setPlainText(QString::fromUtf8(data)); } else if(mime.startsWith("image/")) { QImage img; if(img.loadFromData(data, nullptr)) { - auto * item = this->graphics_scene.addPixmap(QPixmap::fromImage(img)); + this->graphics_scene.addPixmap(QPixmap::fromImage(img)); } else { - auto * item = this->graphics_scene.addText("Failed to load picture!"); + this->graphics_scene.addText("Failed to load picture!"); } this->ui->graphics_browser->fitInView(graphics_scene.sceneRect(), Qt::KeepAspectRatio); @@ -193,8 +167,16 @@ void BrowserTab::on_gemini_complete(const QByteArray &data, const QString &mime) this->ui->text_browser->setText(QString("Unsupported Mime: %1").arg(mime)); } + this->ui->text_browser->setDocument(document.get()); + this->current_document = std::move(document); + this->pushToHistory(this->current_location); + emit this->locationChanged(this->current_location); + + QString title = this->current_location.toString(); + emit this->titleChanged(title); + this->successfully_loaded = true; this->updateUI(); } @@ -386,30 +368,105 @@ void BrowserTab::on_text_browser_highlighted(const QUrl &url) this->mainWindow->setUrlPreview(real_url); } +void BrowserTab::on_back_button_clicked() +{ + +} + +void BrowserTab::on_forward_button_clicked() +{ + +} + void BrowserTab::updateUI() { this->ui->back_button->setEnabled(this->history.canGoBack()); this->ui->forward_button->setEnabled(this->history.canGoForward()); - this->ui->refresh_button->setEnabled(this->successfully_loaded); + this->ui->refresh_button->setVisible(not this->gemini_client.isInProgress()); + this->ui->stop_button->setVisible(this->gemini_client.isInProgress()); this->ui->fav_button->setEnabled(this->successfully_loaded); this->ui->fav_button->setChecked(this->mainWindow->favourites.contains(this->current_location)); } -QByteArray BrowserTab::translateGeminiToHtml(const QByteArray &input, DocumentOutlineModel & outline) +QByteArray trim_whitespace(QByteArray items) { - QByteArray result; - result.append(QString(R"html(<!doctype html> -<html> - <head> - <meta charset="UTF-8"> - </head> - <body> -)html").toUtf8()); + int start = 0; + while(start < items.size() and isspace(items.at(start))) { + start += 1; + } + int end = items.size() - 1; + while(end > 0 and isspace(items.at(end))) { + end -= 1; + } + return items.mid(start, end - start + 1); +} + +std::unique_ptr<QTextDocument> BrowserTab::translateGemini(const QByteArray &input, QUrl const & root_url, DocumentOutlineModel &outline) +{ + QFont preformatted_font; + preformatted_font.setFamily("monospace"); + preformatted_font.setPointSizeF(10.0); + + QFont standard_font; + standard_font.setFamily("sans"); + standard_font.setPointSizeF(10.0); + + QFont h1_font; + h1_font.setFamily("sans"); + h1_font.setBold(true); + h1_font.setPointSizeF(20.0); + + QFont h2_font; + h2_font.setFamily("sans"); + h2_font.setBold(true); + h2_font.setPointSizeF(15.0); + + QFont h3_font; + h3_font.setFamily("sans"); + h3_font.setBold(true); + h3_font.setPointSizeF(12.0); + + QTextCharFormat preformatted; + preformatted.setFont(preformatted_font); + + QTextCharFormat standard; + standard.setFont(standard_font); + + QTextCharFormat standard_link; + standard_link.setFont(standard_font); + standard_link.setForeground(QBrush(QColor(0,128,255))); + + QTextCharFormat external_link; + external_link.setFont(standard_font); + external_link.setForeground(QBrush(QColor(0,0,255))); + + QTextCharFormat cross_protocol_link; + cross_protocol_link.setFont(standard_font); + cross_protocol_link.setForeground(QBrush(QColor(128,0,255))); + + QTextCharFormat standard_h1; + standard_h1.setFont(h1_font); + standard_h1.setForeground(QBrush(QColor(255,0,0))); + + QTextCharFormat standard_h2; + standard_h2.setFont(h2_font); + standard_h2.setForeground(QBrush(QColor(0,128,0))); + + QTextCharFormat standard_h3; + standard_h3.setFont(h3_font); + standard_h3.setForeground(QBrush(QColor(32,255,0))); + + std::unique_ptr<QTextDocument> result = std::make_unique<QTextDocument>(); + result->setDocumentMargin(55.0); + + QTextCursor cursor { result.get() }; + + QTextBlockFormat non_list_format = cursor.blockFormat(); bool verbatim = false; - bool listing = false; + QTextList * current_list = nullptr; outline.beginBuild(); @@ -417,54 +474,53 @@ QByteArray BrowserTab::translateGeminiToHtml(const QByteArray &input, DocumentOu for(auto const & line : lines) { if(verbatim) { - if(listing) { - result.append("</ul>\n"); - } - listing = false; - if(line.startsWith("```")) { verbatim = false; - result.append("</pre><br>\n"); } else { - result.append(line); - result.append("\n"); + cursor.setCharFormat(preformatted); + cursor.insertText(line + "\n"); } } else { if(line.startsWith("*")) { - if(not listing) { - result.append("<ul>\n"); + if(current_list == nullptr) { + cursor.deletePreviousChar(); + current_list = cursor.insertList(QTextListFormat::ListDisc); + } else { + cursor.insertBlock(); } - listing = true; - result.append("<li>"); - result.append(line.mid(1).trimmed()); - result.append("</li>"); + QString item = trim_whitespace(line.mid(1)); + + cursor.insertText(item, standard); continue; } else { - if(listing) { - result.append("</ul>\n"); + if(current_list != nullptr) { + cursor.insertBlock(); + cursor.setBlockFormat(non_list_format); } - listing = false; + current_list = nullptr; } if(line.startsWith("###")) { - result.append("<h3>"); - outline.appendH3(line.mid(3).trimmed()); - result.append(line.mid(3).trimmed()); - result.append("</h3>"); + auto heading = trim_whitespace(line.mid(3)); + + cursor.insertText(heading, standard_h3); + cursor.insertBlock(); + outline.appendH3(heading); } else if(line.startsWith("##")) { - result.append("<h2>"); - outline.appendH2(line.mid(2).trimmed()); - result.append(line.mid(2).trimmed()); - result.append("</h2>"); + auto heading = trim_whitespace(line.mid(2)); + + cursor.insertText(heading, standard_h2); + cursor.insertBlock(); + outline.appendH2(heading); } else if(line.startsWith("#")) { - result.append("<h1>"); - outline.appendH1(line.mid(1).trimmed()); - result.append(line.mid(1).trimmed()); - result.append("</h1>"); + auto heading = trim_whitespace(line.mid(1)); + + cursor.insertText(heading, standard_h1); + outline.appendH1(heading); } else if(line.startsWith("=>")) { auto const part = line.mid(2).trimmed(); @@ -480,39 +536,48 @@ QByteArray BrowserTab::translateGeminiToHtml(const QByteArray &input, DocumentOu } if(index > 0) { - link = part.mid(0, index); - title = part.mid(index + 1); + link = trim_whitespace(part.mid(0, index)); + title = trim_whitespace(part.mid(index + 1)); } else { - link = part; - title = part; + link = trim_whitespace(part); + title = trim_whitespace(part); } + auto local_url = QUrl(link); + + auto absolute_url = root_url.resolved(QUrl(link)); + // qDebug() << link << title; - result.append("<a href=\""); - result.append(link); - result.append("\">"); - result.append(title); - result.append("</a><br>\n"); + auto fmt = standard_link; + if(not local_url.isRelative()) { + fmt = external_link; + } + + QString suffix = ""; + if(absolute_url.scheme() != root_url.scheme()) { + suffix = " [" + absolute_url.scheme().toUpper() + "]"; + fmt = cross_protocol_link; + } + + fmt.setAnchor(true); + fmt.setAnchorHref(absolute_url.toString()); + + if(local_url.isRelative()) { + cursor.insertText("→ " + title + suffix + "\n", fmt); + } else { + cursor.insertText("⇒ " + title + suffix + "\n", fmt); + } } else if(line.startsWith("```")) { verbatim = true; - result.append("<pre>"); } else { - result.append(line); - result.append("<br>\n"); + cursor.insertText(line + "\n", standard); } } } outline.endBuild(); - - result.append(QString(R"html( - </body> -</html> -)html").toUtf8()); return result; } - - diff --git a/browsertab.hpp b/browsertab.hpp index cd31a94..3ef355e 100644 --- a/browsertab.hpp +++ b/browsertab.hpp @@ -4,6 +4,7 @@ #include <QWidget> #include <QUrl> #include <QGraphicsScene> +#include <QTextDocument> #include "geminiclient.hpp" #include "documentoutlinemodel.hpp" @@ -27,19 +28,15 @@ public: void navigateBack(QModelIndex history_index); +signals: + void titleChanged(QString const & title); + void locationChanged(QUrl const & url); + private slots: void on_menu_button_clicked(); void on_url_bar_returnPressed(); - void on_content_titleChanged(const QString &title); - - void on_content_loadStarted(); - - void on_content_loadFinished(bool arg1); - - void on_content_urlChanged(const QUrl &arg1); - void on_refresh_button_clicked(); void on_gemini_complete(QByteArray const & data, QString const & mime); @@ -75,6 +72,10 @@ private slots: void on_text_browser_highlighted(const QUrl &arg1); + void on_back_button_clicked(); + + void on_forward_button_clicked(); + private: void setErrorMessage(QString const & msg); @@ -82,7 +83,7 @@ private: void updateUI(); - static QByteArray translateGeminiToHtml(QByteArray const & input, DocumentOutlineModel & outline); + static std::unique_ptr<QTextDocument> translateGemini(QByteArray const & input, QUrl const & root_url, DocumentOutlineModel & outline); public: Ui::BrowserTab *ui; @@ -97,6 +98,8 @@ public: DocumentOutlineModel outline; QGraphicsScene graphics_scene; TabBrowsingHistory history; + + std::unique_ptr<QTextDocument> current_document; }; #endif // BROWSERTAB_HPP diff --git a/browsertab.ui b/browsertab.ui index d455fae..489472a 100644 --- a/browsertab.ui +++ b/browsertab.ui @@ -72,6 +72,20 @@ </widget> </item> <item> + <widget class="QToolButton" name="stop_button"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/close.svg</normaloff>:/icons/close.svg</iconset> + </property> + </widget> + </item> + <item> <widget class="QToolButton" name="refresh_button"> <property name="enabled"> <bool>false</bool> diff --git a/documentoutlinemodel.cpp b/documentoutlinemodel.cpp index 1787d9e..b44ceff 100644 --- a/documentoutlinemodel.cpp +++ b/documentoutlinemodel.cpp @@ -9,6 +9,13 @@ DocumentOutlineModel::DocumentOutlineModel() : } +void DocumentOutlineModel::clear() +{ + beginBuild(); + endBuild(); + +} + void DocumentOutlineModel::beginBuild() { beginResetModel(); @@ -17,7 +24,7 @@ void DocumentOutlineModel::beginBuild() "<ROOT>", 0, QVector<Node> { }, -}; + }; } void DocumentOutlineModel::appendH1(const QString &title) diff --git a/documentoutlinemodel.hpp b/documentoutlinemodel.hpp index 802bd86..e10163b 100644 --- a/documentoutlinemodel.hpp +++ b/documentoutlinemodel.hpp @@ -10,6 +10,8 @@ class DocumentOutlineModel : public: DocumentOutlineModel(); + void clear(); + void beginBuild(); void appendH1(QString const & title); @@ -6,5 +6,6 @@ <file>icons/heart.svg</file> <file>icons/menu.svg</file> <file>icons/refresh.svg</file> + <file>icons/close.svg</file> </qresource> </RCC> diff --git a/icons/close.svg b/icons/close.svg new file mode 100644 index 0000000..18691d7 --- /dev/null +++ b/icons/close.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg>
\ No newline at end of file diff --git a/mainwindow.cpp b/mainwindow.cpp index 48e7f54..aba06f0 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -32,6 +32,8 @@ BrowserTab * MainWindow::addEmptyTab(bool focus_new) { BrowserTab * tab = new BrowserTab(this); + connect(tab, &BrowserTab::titleChanged, this, &MainWindow::on_tab_titleChanged); + int index = this->ui->browser_tabs->addTab(tab, "Page"); if(focus_new) { @@ -102,3 +104,23 @@ void MainWindow::on_history_view_doubleClicked(const QModelIndex &index) tab->navigateBack(index); } } + +void MainWindow::on_tab_titleChanged(const QString &title) +{ + auto * tab = qobject_cast<BrowserTab*>(sender()); + if(tab != nullptr) { + int index = this->ui->browser_tabs->indexOf(tab); + assert(index >= 0); + this->ui->browser_tabs->setTabText(index, title); + } +} + +void MainWindow::on_tab_locationChanged(const QUrl &url) +{ + auto * tab = qobject_cast<BrowserTab*>(sender()); + if(tab != nullptr) { + int index = this->ui->browser_tabs->indexOf(tab); + assert(index >= 0); + this->ui->browser_tabs->setTabToolTip(index, url.toString()); + } +} diff --git a/mainwindow.hpp b/mainwindow.hpp index c55a569..ea2af3e 100644 --- a/mainwindow.hpp +++ b/mainwindow.hpp @@ -20,7 +20,6 @@ public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); - BrowserTab * addEmptyTab(bool focus_new); BrowserTab * addNewTab(bool focus_new, QUrl const & url); @@ -38,6 +37,10 @@ private slots: void on_history_view_doubleClicked(const QModelIndex &index); + void on_tab_titleChanged(QString const & title); + + void on_tab_locationChanged(QUrl const & url); + private: Ui::MainWindow *ui; |
