From 3aed883402dc8da829fc304434c5efd0570cbb97 Mon Sep 17 00:00:00 2001 From: "Felix (xq) Queißner" Date: Sat, 6 Jun 2020 23:14:21 +0200 Subject: Moves source code into subdirectory. --- README.md | 3 +- browsertab.cpp | 405 --------------------------------- browsertab.hpp | 114 ---------- browsertab.ui | 187 ---------------- documentoutlinemodel.cpp | 200 ----------------- documentoutlinemodel.hpp | 57 ----- favouritecollection.cpp | 140 ------------ favouritecollection.hpp | 44 ---- geminiclient.cpp | 252 --------------------- geminiclient.hpp | 89 -------- geminirenderer.cpp | 404 --------------------------------- geminirenderer.hpp | 76 ------- icons.qrc | 15 -- icons/arrow-left.svg | 1 - icons/arrow-right.svg | 1 - icons/close.svg | 1 - icons/format-font.svg | 1 - icons/heart-outline.svg | 1 - icons/heart.svg | 1 - icons/kristall.svg | 13 -- icons/menu.svg | 1 - icons/palette.svg | 1 - icons/refresh.svg | 1 - icons/settings.svg | 1 - kristall.pro | 53 ----- kristall_en_US.ts | 3 - main.cpp | 18 -- mainwindow.cpp | 259 ---------------------- mainwindow.hpp | 78 ------- mainwindow.ui | 309 -------------------------- settingsdialog.cpp | 255 --------------------- settingsdialog.hpp | 79 ------- settingsdialog.ui | 517 ------------------------------------------- src/browsertab.cpp | 405 +++++++++++++++++++++++++++++++++ src/browsertab.hpp | 114 ++++++++++ src/browsertab.ui | 187 ++++++++++++++++ src/documentoutlinemodel.cpp | 200 +++++++++++++++++ src/documentoutlinemodel.hpp | 57 +++++ src/favouritecollection.cpp | 140 ++++++++++++ src/favouritecollection.hpp | 44 ++++ src/geminiclient.cpp | 252 +++++++++++++++++++++ src/geminiclient.hpp | 89 ++++++++ src/geminirenderer.cpp | 404 +++++++++++++++++++++++++++++++++ src/geminirenderer.hpp | 76 +++++++ src/icons.qrc | 15 ++ src/icons/arrow-left.svg | 1 + src/icons/arrow-right.svg | 1 + src/icons/close.svg | 1 + src/icons/format-font.svg | 1 + src/icons/heart-outline.svg | 1 + src/icons/heart.svg | 1 + src/icons/kristall.svg | 13 ++ src/icons/menu.svg | 1 + src/icons/palette.svg | 1 + src/icons/refresh.svg | 1 + src/icons/settings.svg | 1 + src/kristall.pro | 53 +++++ src/kristall_en_US.ts | 3 + src/main.cpp | 18 ++ src/mainwindow.cpp | 259 ++++++++++++++++++++++ src/mainwindow.hpp | 78 +++++++ src/mainwindow.ui | 309 ++++++++++++++++++++++++++ src/settingsdialog.cpp | 255 +++++++++++++++++++++ src/settingsdialog.hpp | 79 +++++++ src/settingsdialog.ui | 517 +++++++++++++++++++++++++++++++++++++++++++ src/tabbrowsinghistory.cpp | 79 +++++++ src/tabbrowsinghistory.hpp | 39 ++++ tabbrowsinghistory.cpp | 79 ------- tabbrowsinghistory.hpp | 39 ---- 69 files changed, 3697 insertions(+), 3696 deletions(-) delete mode 100644 browsertab.cpp delete mode 100644 browsertab.hpp delete mode 100644 browsertab.ui delete mode 100644 documentoutlinemodel.cpp delete mode 100644 documentoutlinemodel.hpp delete mode 100644 favouritecollection.cpp delete mode 100644 favouritecollection.hpp delete mode 100644 geminiclient.cpp delete mode 100644 geminiclient.hpp delete mode 100644 geminirenderer.cpp delete mode 100644 geminirenderer.hpp delete mode 100644 icons.qrc delete mode 100644 icons/arrow-left.svg delete mode 100644 icons/arrow-right.svg delete mode 100644 icons/close.svg delete mode 100644 icons/format-font.svg delete mode 100644 icons/heart-outline.svg delete mode 100644 icons/heart.svg delete mode 100644 icons/kristall.svg delete mode 100644 icons/menu.svg delete mode 100644 icons/palette.svg delete mode 100644 icons/refresh.svg delete mode 100644 icons/settings.svg delete mode 100644 kristall.pro delete mode 100644 kristall_en_US.ts delete mode 100644 main.cpp delete mode 100644 mainwindow.cpp delete mode 100644 mainwindow.hpp delete mode 100644 mainwindow.ui delete mode 100644 settingsdialog.cpp delete mode 100644 settingsdialog.hpp delete mode 100644 settingsdialog.ui create mode 100644 src/browsertab.cpp create mode 100644 src/browsertab.hpp create mode 100644 src/browsertab.ui create mode 100644 src/documentoutlinemodel.cpp create mode 100644 src/documentoutlinemodel.hpp create mode 100644 src/favouritecollection.cpp create mode 100644 src/favouritecollection.hpp create mode 100644 src/geminiclient.cpp create mode 100644 src/geminiclient.hpp create mode 100644 src/geminirenderer.cpp create mode 100644 src/geminirenderer.hpp create mode 100644 src/icons.qrc create mode 100644 src/icons/arrow-left.svg create mode 100644 src/icons/arrow-right.svg create mode 100644 src/icons/close.svg create mode 100644 src/icons/format-font.svg create mode 100644 src/icons/heart-outline.svg create mode 100644 src/icons/heart.svg create mode 100644 src/icons/kristall.svg create mode 100644 src/icons/menu.svg create mode 100644 src/icons/palette.svg create mode 100644 src/icons/refresh.svg create mode 100644 src/icons/settings.svg create mode 100644 src/kristall.pro create mode 100644 src/kristall_en_US.ts create mode 100644 src/main.cpp create mode 100644 src/mainwindow.cpp create mode 100644 src/mainwindow.hpp create mode 100644 src/mainwindow.ui create mode 100644 src/settingsdialog.cpp create mode 100644 src/settingsdialog.hpp create mode 100644 src/settingsdialog.ui create mode 100644 src/tabbrowsinghistory.cpp create mode 100644 src/tabbrowsinghistory.hpp delete mode 100644 tabbrowsinghistory.cpp delete mode 100644 tabbrowsinghistory.hpp diff --git a/README.md b/README.md index 19e4ceb..cc0e7e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A high-quality visual cross-platform gemini browser. - `text/gemini` - `text/html` (reduced feature set) - `text/markdown` - - `text/*` + - `text/*` - `image/*` - [Outline generation](https://mq32.de/public/a50ef327f4150d870393b1989c5b41db495b56f7.png) ([Video](https://mq32.de/public/kristall-02.mp4)) - Favourite Sites @@ -67,5 +67,6 @@ Notes for OpenBSD: - [ ] Add favicon support - [ ] Add auto-generated "favicons" - [ ] Check if the site follows this guideline: `# Title` where `` is a unicode emoji + - [ ] Opt-In: Regularly check for `domain/favicon.txt` - [ ] Opt-In: Regularly check for `domain/favicon.ico` diff --git a/browsertab.cpp b/browsertab.cpp deleted file mode 100644 index f7e5f78..0000000 --- a/browsertab.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include "browsertab.hpp" -#include "ui_browsertab.h" -#include "mainwindow.hpp" -#include "geminirenderer.hpp" -#include "settingsdialog.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - - -BrowserTab::BrowserTab(MainWindow * mainWindow) : - QWidget(nullptr), - ui(new Ui::BrowserTab), - mainWindow(mainWindow), - outline(), - graphics_scene() -{ - ui->setupUi(this); - - connect(&gemini_client, &GeminiClient::requestComplete, this, &BrowserTab::on_gemini_complete); - connect(&gemini_client, &GeminiClient::protocolViolation, this, &BrowserTab::on_protocolViolation); - connect(&gemini_client, &GeminiClient::inputRequired, this, &BrowserTab::on_inputRequired); - connect(&gemini_client, &GeminiClient::redirected, this, &BrowserTab::on_redirected); - connect(&gemini_client, &GeminiClient::temporaryFailure, this, &BrowserTab::on_temporaryFailure); - connect(&gemini_client, &GeminiClient::permanentFailure, this, &BrowserTab::on_permanentFailure); - connect(&gemini_client, &GeminiClient::transientCertificateRequested, this, &BrowserTab::on_transientCertificateRequested); - connect(&gemini_client, &GeminiClient::authorisedCertificateRequested, this, &BrowserTab::on_authorisedCertificateRequested); - connect(&gemini_client, &GeminiClient::certificateRejected, this, &BrowserTab::on_certificateRejected); - - this->updateUI(); - - this->ui->graphics_browser->setVisible(false); - this->ui->text_browser->setVisible(false); - - this->ui->graphics_browser->setScene(&graphics_scene); -} - -BrowserTab::~BrowserTab() -{ - delete ui; -} - -void BrowserTab::navigateTo(const QUrl &url, PushToHistory mode) -{ - if(url.scheme() != "gemini") { - QMessageBox::warning(this, "Kristall", "Unsupported uri scheme: " + url.scheme()); - return; - } - this->current_location = url; - this->ui->url_bar->setText(url.toString()); - - if(not gemini_client.cancelRequest()) { - QMessageBox::warning(this, "Kristall", "Unsupported uri scheme: " + url.scheme()); - return; - } - - this->redirection_count = 0; - this->successfully_loaded = false; - this->push_to_history_after_load = (mode == PushAfterSuccess); - - gemini_client.startRequest(url); - - - switch(mode) - { - case DontPush: - case PushAfterSuccess: - break; - - case PushImmediate: - pushToHistory(url); - break; - } - - this->updateUI(); -} - -void BrowserTab::navigateBack(QModelIndex history_index) -{ - auto url = history.get(history_index); - - if(url.isValid()) { - current_history_index = history_index; - navigateTo(url, DontPush); - } -} - -void BrowserTab::navOneBackback() -{ - navigateBack(history.oneBackward(current_history_index)); -} - -void BrowserTab::navOneForward() -{ - navigateBack(history.oneForward(current_history_index)); -} - -void BrowserTab::scrollToAnchor(QString const & anchor) -{ - qDebug() << "scroll to anchor" << anchor; - this->ui->text_browser->scrollToAnchor(anchor); -} - -void BrowserTab::reloadPage() -{ - if(current_location.isValid()) - this->navigateTo(this->current_location, DontPush); -} - -void BrowserTab::on_url_bar_returnPressed() -{ - QUrl url { this->ui->url_bar->text() }; - - if(url.scheme().isEmpty()) { - url = QUrl { "gemini://" + this->ui->url_bar->text() }; - } - - this->navigateTo(url, PushImmediate); -} - -void BrowserTab::on_refresh_button_clicked() -{ - reloadPage(); -} - -void BrowserTab::on_gemini_complete(const QByteArray &data, const QString &mime) -{ - qDebug() << "Loaded" << data.length() << "bytes of type" << mime; - - - this->graphics_scene.clear(); - this->ui->text_browser->setText(""); - - this->ui->text_browser->setVisible(mime.startsWith("text/")); - this->ui->graphics_browser->setVisible(mime.startsWith("image/")); - - ui->text_browser->setStyleSheet(""); - - std::unique_ptr document; - - this->outline.clear(); - - if(mime.startsWith("text/gemini")) { - - auto doc= GeminiRenderer{ mainWindow->current_style }.render(data, this->current_location, this->outline); - this->ui->text_browser->setStyleSheet(QString("QTextBrowser { background-color: %1; }").arg(doc->background_color.name())); - - document = std::move(doc); - } - else if(mime.startsWith("text/html")) { - document = std::make_unique(); - document->setHtml(QString::fromUtf8(data)); - } -#if defined(QT_FEATURE_textmarkdownreader) - else if(mime.startsWith("text/markdown")) { - document = std::make_unique(); - document->setMarkdown(QString::fromUtf8(data)); - } -#endif - else if(mime.startsWith("text/")) { - QFont monospace; - monospace.setFamily("monospace"); - - document = std::make_unique(); - document->setDefaultFont(monospace); - document->setPlainText(QString::fromUtf8(data)); - } - else if(mime.startsWith("image/")) { - - QImage img; - if(img.loadFromData(data, nullptr)) - { - this->graphics_scene.addPixmap(QPixmap::fromImage(img)); - } - else - { - this->graphics_scene.addText("Failed to load picture!"); - } - - this->ui->graphics_browser->fitInView(graphics_scene.sceneRect(), Qt::KeepAspectRatio); - - } - else { - this->ui->text_browser->setVisible(true); - this->ui->text_browser->setText(QString("Unsupported Mime: %1").arg(mime)); - } - - this->ui->text_browser->setDocument(document.get()); - this->current_document = std::move(document); - - emit this->locationChanged(this->current_location); - - QString title = this->current_location.toString(); - emit this->titleChanged(title); - - this->successfully_loaded = true; - - if(this->push_to_history_after_load) { - this->pushToHistory(this->current_location); - this->push_to_history_after_load = false; - } - - this->updateUI(); -} - -void BrowserTab::on_protocolViolation(const QString &reason) -{ - this->setErrorMessage(QString("Protocol violation:\n%1").arg(reason)); -} - -void BrowserTab::on_inputRequired(const QString &query) -{ - QInputDialog dialog { this }; - - dialog.setInputMode(QInputDialog::TextInput); - dialog.setLabelText(query); - - if(dialog.exec() != QDialog::Accepted) { - setErrorMessage(QString("Site requires input:\n%1").arg(query)); - return; - } - - QUrl new_location = current_location; - new_location.setQuery(dialog.textValue()); - this->navigateTo(new_location, DontPush); -} - -void BrowserTab::on_redirected(const QUrl &uri, bool is_permanent) -{ - if(redirection_count >= 5) { - setErrorMessage("Too many redirections!"); - return; - } - else { - if(gemini_client.startRequest(uri)) { - redirection_count += 1; - this->current_location = uri; - this->ui->url_bar->setText(uri.toString()); - } - } -} - -void BrowserTab::on_temporaryFailure(TemporaryFailure reason, const QString &info) -{ - switch(reason) - { - case TemporaryFailure::cgi_error: - setErrorMessage(QString("CGI Error\n%1").arg(info)); - break; - case TemporaryFailure::slow_down: - setErrorMessage(QString("Slow Down\n%1").arg(info)); - break; - case TemporaryFailure::proxy_error: - setErrorMessage(QString("Proxy Error\n%1").arg(info)); - break; - case TemporaryFailure::unspecified: - setErrorMessage(QString("Temporary Failure\n%1").arg(info)); - break; - case TemporaryFailure::server_unavailable: - setErrorMessage(QString("Server Unavailable\n%1").arg(info)); - break; - } -} - -void BrowserTab::on_permanentFailure(PermanentFailure reason, const QString &info) -{ - switch(reason) - { - case PermanentFailure::gone: - setErrorMessage(QString("Gone\n%1").arg(info)); - break; - case PermanentFailure::not_found: - setErrorMessage(QString("Not Found\n%1").arg(info)); - break; - case PermanentFailure::bad_request: - setErrorMessage(QString("Bad Request\n%1").arg(info)); - break; - case PermanentFailure::unspecified: - setErrorMessage(QString("Permanent Failure\n%1").arg(info)); - break; - case PermanentFailure::proxy_request_required: - setErrorMessage(QString("Proxy Request Required\n%1").arg(info)); - break; - } -} - -void BrowserTab::on_transientCertificateRequested(const QString &reason) -{ - QMessageBox::warning(this, "Kristall", "Transient certificate requirested:\n" + reason); - this->updateUI(); -} - -void BrowserTab::on_authorisedCertificateRequested(const QString &reason) -{ - QMessageBox::warning(this, "Kristall", "Authorized certificate requirested:\n" + reason); - this->updateUI(); -} - -void BrowserTab::on_certificateRejected(CertificateRejection reason, const QString &info) -{ - switch(reason) - { - case CertificateRejection::unspecified: - setErrorMessage(QString("Certificate Rejected\n%1").arg(info)); - break; - case CertificateRejection::not_accepted: - setErrorMessage(QString("Certificate not accepted\n%1").arg(info)); - break; - case CertificateRejection::future_certificate_rejected: - setErrorMessage(QString("Certificate is not yet valid\n%1").arg(info)); - break; - case CertificateRejection::expired_certificate_rejected: - setErrorMessage(QString("Certificate expired\n%1").arg(info)); - break; - } -} - -void BrowserTab::on_linkHovered(const QString &url) -{ - this->mainWindow->setUrlPreview(QUrl(url)); -} - -void BrowserTab::setErrorMessage(const QString &msg) -{ - // this->page.setContent(QString("An error happened:\n%0").arg(msg).toUtf8(), "text/plain charset=utf-8"); - QMessageBox::warning(this, "Kristall", msg); - this->updateUI(); -} - -void BrowserTab::pushToHistory(const QUrl &url) -{ - this->current_history_index = this->history.pushUrl(this->current_history_index, url); - this->updateUI(); -} - -void BrowserTab::on_fav_button_clicked() -{ - if(this->ui->fav_button->isChecked()) { - this->mainWindow->favourites.add(this->current_location); - } else { - this->mainWindow->favourites.remove(this->current_location); - } - - this->updateUI(); -} - - -void BrowserTab::on_text_browser_anchorClicked(const QUrl &url) -{ - qDebug() << url; - - QUrl real_url = url; - if(real_url.isRelative()) - real_url = this->current_location.resolved(url); - - if(real_url.scheme() != "gemini") { - QMessageBox::warning(this, "Kristall", QString("Unsupported url: %1").arg(real_url.toString())); - } - else { - this->navigateTo(real_url, PushAfterSuccess); - } -} - -void BrowserTab::on_text_browser_highlighted(const QUrl &url) -{ - QUrl real_url = url; - if(real_url.isRelative()) - real_url = this->current_location.resolved(url); - this->mainWindow->setUrlPreview(real_url); -} - -void BrowserTab::on_stop_button_clicked() -{ - gemini_client.cancelRequest(); -} - -void BrowserTab::on_back_button_clicked() -{ - navOneBackback(); -} - -void BrowserTab::on_forward_button_clicked() -{ - navOneForward(); -} - -void BrowserTab::updateUI() -{ - this->ui->back_button->setEnabled(history.oneBackward(current_history_index).isValid()); - this->ui->forward_button->setEnabled(history.oneForward(current_history_index).isValid()); - - this->ui->refresh_button->setVisible(this->successfully_loaded); - this->ui->stop_button->setVisible(not this->successfully_loaded); - - this->ui->fav_button->setEnabled(this->successfully_loaded); - this->ui->fav_button->setChecked(this->mainWindow->favourites.contains(this->current_location)); -} diff --git a/browsertab.hpp b/browsertab.hpp deleted file mode 100644 index 7f0ac35..0000000 --- a/browsertab.hpp +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef BROWSERTAB_HPP -#define BROWSERTAB_HPP - -#include -#include -#include -#include - -#include "geminiclient.hpp" -#include "documentoutlinemodel.hpp" -#include "tabbrowsinghistory.hpp" -#include "geminirenderer.hpp" - -namespace Ui { -class BrowserTab; -} - -class MainWindow; - -class BrowserTab : public QWidget -{ - Q_OBJECT -public: - enum PushToHistory { - DontPush, - PushImmediate, - PushAfterSuccess, - }; - -public: - explicit BrowserTab(MainWindow * mainWindow); - ~BrowserTab(); - - void navigateTo(QUrl const & url, PushToHistory mode); - - void navigateBack(QModelIndex history_index); - - void navOneBackback(); - - void navOneForward(); - - void scrollToAnchor(QString const & anchor); - - void reloadPage(); - -signals: - void titleChanged(QString const & title); - void locationChanged(QUrl const & url); - -private slots: - void on_url_bar_returnPressed(); - - void on_refresh_button_clicked(); - - void on_gemini_complete(QByteArray const & data, QString const & mime); - - void on_protocolViolation(QString const & reason); - - void on_inputRequired(QString const & query); - - void on_redirected(QUrl const & uri, bool is_permanent); - - void on_temporaryFailure(TemporaryFailure reason, QString const & info); - - void on_permanentFailure(PermanentFailure reason, QString const & info); - - void on_transientCertificateRequested(QString const & reason); - - void on_authorisedCertificateRequested(QString const & reason); - - void on_certificateRejected(CertificateRejection reason, QString const & info); - - void on_linkHovered(const QString &url); - - void on_fav_button_clicked(); - - void on_text_browser_anchorClicked(const QUrl &arg1); - - void on_text_browser_highlighted(const QUrl &arg1); - - void on_back_button_clicked(); - - void on_forward_button_clicked(); - - void on_stop_button_clicked(); - -private: - void setErrorMessage(QString const & msg); - - void pushToHistory(QUrl const & url); - - void updateUI(); - -public: - - Ui::BrowserTab *ui; - MainWindow * mainWindow; - QUrl current_location; - - GeminiClient gemini_client; - int redirection_count = 0; - - bool push_to_history_after_load = false; - bool successfully_loaded = false; - - DocumentOutlineModel outline; - QGraphicsScene graphics_scene; - TabBrowsingHistory history; - QModelIndex current_history_index; - - std::unique_ptr current_document; -}; - -#endif // BROWSERTAB_HPP diff --git a/browsertab.ui b/browsertab.ui deleted file mode 100644 index 265560e..0000000 --- a/browsertab.ui +++ /dev/null @@ -1,187 +0,0 @@ - - - BrowserTab - - - - 0 - 0 - 692 - 404 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QLayout::SetDefaultConstraint - - - 9 - - - 9 - - - 9 - - - 9 - - - - - false - - - - - - - :/icons/arrow-left.svg:/icons/arrow-left.svg - - - - - - - false - - - ... - - - - :/icons/arrow-right.svg:/icons/arrow-right.svg - - - - - - - true - - - - - - - :/icons/close.svg:/icons/close.svg - - - - - - - true - - - - - - - :/icons/refresh.svg:/icons/refresh.svg - - - - - - - gemini:// - - - - - - - false - - - - - - - :/icons/heart-outline.svg - :/icons/heart.svg:/icons/heart-outline.svg - - - true - - - false - - - - - - - - - QLayout::SetDefaultConstraint - - - - - - - - QTextEdit::AutoNone - - - true - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Start surfin!</p></body></html> - - - 40 - - - false - - - - - - - QGraphicsView::ScrollHandDrag - - - QGraphicsView::AnchorUnderMouse - - - QGraphicsView::AnchorUnderMouse - - - - - - - - - - - - diff --git a/documentoutlinemodel.cpp b/documentoutlinemodel.cpp deleted file mode 100644 index 5978f33..0000000 --- a/documentoutlinemodel.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "documentoutlinemodel.hpp" - -#include - -DocumentOutlineModel::DocumentOutlineModel() : - QAbstractItemModel(), - root() -{ - -} - -void DocumentOutlineModel::clear() -{ - beginBuild(); - endBuild(); - -} - -void DocumentOutlineModel::beginBuild() -{ - beginResetModel(); - root = Node { - nullptr, - "", "", - 0, 0, - QList { }, - }; -} - -void DocumentOutlineModel::appendH1(const QString &title, QString const & anchor) -{ - root.children.append(Node { - &root, - title, anchor, - 1, 0, - QList { }, - }); -} - -void DocumentOutlineModel::appendH2(const QString &title, QString const & anchor) -{ - auto & parent = ensureLevel1(); - parent.children.append(Node { - &parent, - title, anchor, - 2, parent.children.size() - 1, - QList { }, - }); -} - -void DocumentOutlineModel::appendH3(const QString &title, QString const & anchor) -{ - auto & parent = ensureLevel2(); - parent.children.append(Node { - &parent, - title, anchor, - 3, parent.children.size() - 1, - QList { }, - }); -} - -void DocumentOutlineModel::endBuild() -{ - for(auto const & h1 : this->root.children) - { - assert(h1.depth == 1); - assert(h1.parent == &this->root); - for(auto const & h2 : h1.children) - { - assert(h2.depth == 2); - assert(h2.parent == &h1); - for(auto const & h3 : h2.children) - { - assert(h3.depth == 3); - assert(h3.parent == &h2); - } - } - } - endResetModel(); -} - -QString DocumentOutlineModel::getTitle(const QModelIndex &index) const -{ - if(not index.isValid()) - return ""; - - Node const *childItem = static_cast(index.internalPointer()); - - return childItem->title; -} - -QString DocumentOutlineModel::getAnchor(const QModelIndex &index) const -{ - if(not index.isValid()) - return ""; - - Node const *childItem = static_cast(index.internalPointer()); - - return childItem->anchor; -} - -QModelIndex DocumentOutlineModel::index(int row, int column, const QModelIndex &parent) const -{ - if (not hasIndex(row, column, parent)) - return QModelIndex(); - - Node const * parentItem; - - if (!parent.isValid()) - parentItem = &this->root; - else - parentItem = static_cast(parent.internalPointer()); - - Node const * childItem = &parentItem->children[row]; - if (childItem) - return createIndex(row, column, reinterpret_cast(childItem)); - return QModelIndex(); - -} - -QModelIndex DocumentOutlineModel::parent(const QModelIndex &child) const -{ - if (!child.isValid()) - return QModelIndex(); - - Node const *childItem = static_cast(child.internalPointer()); - Node const * parent = childItem->parent; - - if (parent == &root) - return QModelIndex(); - - return createIndex( - parent->index, - 0, - reinterpret_cast(parent)); -} - -int DocumentOutlineModel::rowCount(const QModelIndex &parent) const -{ - Node const *parentItem; - if (parent.column() > 0) - return 0; - - if (!parent.isValid()) - parentItem = &root; - else - parentItem = static_cast(parent.internalPointer()); - - return parentItem->children.size(); -} - -int DocumentOutlineModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} - -QVariant DocumentOutlineModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - if (index.column() != 0) - return QVariant(); - - if (role != Qt::DisplayRole) - return QVariant(); - - Node const *item = static_cast(index.internalPointer()); - - return item->title; -} - -DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel1() -{ - if(root.children.size() == 0) { - root.children.append(Node { - &root, - "", "", - 1, 0, - QList { }, - }); - } - return root.children.last(); -} - -DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel2() -{ - auto & parent = ensureLevel1(); - - if(parent.children.size() == 0) { - root.children.append(Node { - &parent, - "", "", - 2, 0, - QList { }, - }); - } - - return parent.children.last(); -} diff --git a/documentoutlinemodel.hpp b/documentoutlinemodel.hpp deleted file mode 100644 index 0476892..0000000 --- a/documentoutlinemodel.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef DOCUMENTOUTLINEMODEL_HPP -#define DOCUMENTOUTLINEMODEL_HPP - -#include -#include - -class DocumentOutlineModel : - public QAbstractItemModel -{ - Q_OBJECT -public: - DocumentOutlineModel(); - - void clear(); - - void beginBuild(); - - void appendH1(QString const & title, QString const & anchor); - - void appendH2(QString const & title, QString const & anchor); - - void appendH3(QString const & title, QString const & anchor); - - void endBuild(); - - QString getTitle(QModelIndex const & index) const; - QString getAnchor(QModelIndex const & index) const; - -public: - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; - - QModelIndex parent(const QModelIndex &child) const override; - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - -private: - struct Node - { - Node * parent; - QString title; - QString anchor; - int depth = 0; - int index = 0; - QList children; - }; - - Node root; - - Node & ensureLevel1(); - Node & ensureLevel2(); -}; - -#endif // DOCUMENTOUTLINEMODEL_HPP diff --git a/favouritecollection.cpp b/favouritecollection.cpp deleted file mode 100644 index 161eacf..0000000 --- a/favouritecollection.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "favouritecollection.hpp" - -#include - -FavouriteCollection::FavouriteCollection(QObject *parent) : - QAbstractListModel(parent) -{ - -} - -void FavouriteCollection::add(QUrl const & url) -{ - if(contains(url)) - return; - - beginInsertRows(QModelIndex{}, items.size(), items.size() + 1); - items.push_back(url); - endInsertRows(); -} - -void FavouriteCollection::remove(QUrl const & url) -{ - for(int i = 0; i < items.size(); i++) - { - if(items.at(i) == url) { - beginRemoveRows(QModelIndex{}, i, i + 1); - items.removeAt(i); - endRemoveRows(); - return; - } - } -} - -bool FavouriteCollection::contains(const QUrl &url) -{ - for(auto const & item : items) { - if(item == url) - return true; - } - return false; -} - -QUrl FavouriteCollection::get(const QModelIndex &index) const -{ - if(index.isValid()) { - return items.at(index.row()); - } else { - return QUrl { }; - } -} - -bool FavouriteCollection::save(const QString &fileName) const -{ - QFile file(fileName); - if(not file.open(QFile::WriteOnly)) - return false; - - for(auto const & url: items) - { - QByteArray blob = (url.toString() + "\n").toUtf8(); - - qint64 offset = 0; - while(offset < blob.size()) - { - auto len = file.write(blob.data() + offset, blob.size() - offset); - if(len <= 0) { - file.close(); - return false; - } - offset += len; - } - } - - file.close(); - return true; -} - -bool FavouriteCollection::save(QSettings &settings) const -{ - settings.beginWriteArray("favourites", items.size()); - for(int i = 0; i < items.size(); i++) - { - settings.setArrayIndex(i); - settings.setValue("url", items[i].toString()); - } - settings.endArray(); - return true; -} - -bool FavouriteCollection::load(const QString &fileName) -{ - QFile file(fileName); - if(not file.open(QFile::ReadOnly)) - return false; - auto data = file.readAll(); - - beginResetModel(); - - items.clear(); - for(auto line : data.split('\n')) { - if(line.size() > 0) { - items.push_back(QUrl(QString::fromUtf8(line))); - } - } - endResetModel(); - - return true; -} - -bool FavouriteCollection::load(QSettings & settings) -{ - int len = settings.beginReadArray("favourites"); - items.resize(len); - for(int i = 0; i < items.size(); i++) - { - settings.setArrayIndex(i); - items[i] = settings.value("url").toString(); - } - settings.endArray(); - return true; -} - -int FavouriteCollection::rowCount(const QModelIndex &parent) const -{ - return items.size(); -} - -bool FavouriteCollection::setData(const QModelIndex &index, const QVariant &value, int role) -{ - return false; -} - -QVariant FavouriteCollection::data(const QModelIndex &index, int role) const -{ - if(role != Qt::DisplayRole) { - return QVariant{}; - } - return items.at(index.row()).toString(); -} - diff --git a/favouritecollection.hpp b/favouritecollection.hpp deleted file mode 100644 index 73afa1f..0000000 --- a/favouritecollection.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef FAVOURITECOLLECTION_HPP -#define FAVOURITECOLLECTION_HPP - -#include -#include -#include -#include - - -class FavouriteCollection : public QAbstractListModel -{ - Q_OBJECT -public: - explicit FavouriteCollection(QObject *parent = nullptr); - - void add(QUrl const & url); - - void remove(QUrl const & url); - - bool contains(QUrl const & url); - - QUrl get(QModelIndex const & index) const ; - - bool save(QString const & fileName) const; - bool save(QSettings & settings) const; - - bool load(QString const & fileName); - bool load(QSettings & settings); - -public: - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - -signals: - -private: - QVector items; - -}; - -#endif // FAVOURITECOLLECTION_HPP diff --git a/geminiclient.cpp b/geminiclient.cpp deleted file mode 100644 index 44fa864..0000000 --- a/geminiclient.cpp +++ /dev/null @@ -1,252 +0,0 @@ -#include "geminiclient.hpp" - -#include - -GeminiClient::GeminiClient(QObject *parent) : QObject(parent) -{ - connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); - connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); - connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); - connect(&socket, QOverload &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); - connect(&socket, QOverload::of(&QSslSocket::error), this, &GeminiClient::socketError); -} - -GeminiClient::~GeminiClient() -{ - is_receiving_body = false; -} - -bool GeminiClient::startRequest(const QUrl &url) -{ - if(socket.isOpen()) - return false; - - socket.connectToHostEncrypted(url.host(), url.port(1965)); - - buffer.clear(); - body.clear(); - is_receiving_body = false; - - if(not socket.isOpen()) - return false; - - target_url = url; - mime_type = ""; - - return true; -} - -bool GeminiClient::isInProgress() const -{ - return socket.isOpen(); -} - -bool GeminiClient::cancelRequest() -{ - this->is_receiving_body = false; - this->socket.close(); - this->buffer.clear(); - this->body.clear(); - return true; -} - -void GeminiClient::socketEncrypted() -{ - QString request = target_url.toString() + "\r\n"; - - QByteArray request_bytes = request.toUtf8(); - - qint64 offset = 0; - while(offset < request_bytes.size()) { - auto const len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset); - if(len <= 0) - { - socket.close(); - return; - } - offset += len; - } -} - -void GeminiClient::socketReadyRead() -{ - QByteArray response = socket.readAll(); - - if(is_receiving_body) - { - body.append(response); - } - else - { - for(int i = 0; i < response.size(); i++) - { - if(response[i] == '\n') { - buffer.append(response.data(), i); - body.append(response.data() + i + 1, response.size() - i - 1); - - // "XY " - if(buffer.size() <= 5) { - socket.close(); - qDebug() << buffer; - emit protocolViolation("Line is too short for valid protocol"); - return; - } - if(buffer[buffer.size() - 1] != '\r') { - socket.close(); - qDebug() << buffer; - emit protocolViolation("Line does not end with "); - return; - } - if(not isdigit(buffer[0])) { - socket.close(); - qDebug() << buffer; - emit protocolViolation("First character is not a digit."); - return; - } - if(not isdigit(buffer[1])) { - socket.close(); - qDebug() << buffer; - emit protocolViolation("Second character is not a digit."); - return; - } - // TODO: Implement stricter version - // if(buffer[2] != ' ') { - if(not isspace(buffer[2])) { - socket.close(); - qDebug() << buffer; - emit protocolViolation("Third character is not a space."); - return; - } - - QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4); - - int primary_code = buffer[0] - '0'; - int secondary_code = buffer[1] - '0'; - - qDebug() << primary_code << secondary_code << meta; - - // We don't need to receive any data after that. - if(primary_code != 2) - socket.close(); - - switch(primary_code) - { - case 1: // requesting input - emit inputRequired(meta); - return; - - case 2: // success - is_receiving_body = true; - mime_type = meta; - return; - - case 3: { // redirect - QUrl new_url(meta); - if(new_url.isValid()) { - if(new_url.isRelative()) - new_url = target_url.resolved(new_url); - assert(not new_url.isRelative()); - - emit redirected(new_url, (secondary_code == 1)); - } - else { - emit protocolViolation("Invalid URL for redirection!"); - } - return; - } - - case 4: { // temporary failure - TemporaryFailure type = TemporaryFailure::unspecified; - switch(secondary_code) - { - case 1: type = TemporaryFailure::server_unavailable; break; - case 2: type = TemporaryFailure::cgi_error; break; - case 3: type = TemporaryFailure::proxy_error; break; - case 4: type = TemporaryFailure::slow_down; break; - } - emit temporaryFailure(type, meta); - return; - } - - case 5: { // permanent failure - PermanentFailure type = PermanentFailure::unspecified; - switch(secondary_code) - { - case 1: type = PermanentFailure::not_found; break; - case 2: type = PermanentFailure::gone; break; - case 3: type = PermanentFailure::proxy_request_required; break; - case 9: type = PermanentFailure::bad_request; break; - } - emit permanentFailure(type, meta); - return; - } - - case 6: // client certificate required - switch(secondary_code) - { - case 1: - emit transientCertificateRequested(meta); - return; - - case 2: - emit authorisedCertificateRequested(meta); - return; - - case 3: - emit certificateRejected(CertificateRejection::not_accepted, meta); - return; - - case 4: - emit certificateRejected(CertificateRejection::future_certificate_rejected, meta); - return; - - case 5: - emit certificateRejected(CertificateRejection::expired_certificate_rejected, meta); - return; - - default: - emit certificateRejected(CertificateRejection::unspecified, meta); - return; - } - return; - - default: - emit protocolViolation("Unspecified status code used!"); - return; - } - - assert(false and "unreachable"); - } - } - buffer.append(response); - } -} - -void GeminiClient::socketDisconnected() -{ - if(is_receiving_body) { - body.append(socket.readAll()); - emit requestComplete(body, mime_type); - } -} - -void GeminiClient::sslErrors(const QList &errors) -{ - for(auto const & error : errors) { - qWarning() << error.errorString() ; - } - - socket.ignoreSslErrors(errors); -} - -void GeminiClient::socketError(QAbstractSocket::SocketError socketError) -{ - // When remote host closes TLS session, the client closes the socket. - // This is more sane then erroring out here as it's a perfectly legal - // state and we know the TLS connection has ended. - if(socketError == QAbstractSocket::RemoteHostClosedError) { - socket.close(); - } else { - qWarning() << socketError << socket.errorString(); - } -} diff --git a/geminiclient.hpp b/geminiclient.hpp deleted file mode 100644 index 590eb5b..0000000 --- a/geminiclient.hpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef GEMINICLIENT_HPP -#define GEMINICLIENT_HPP - -#include -#include -#include -#include - -enum class TemporaryFailure { - unspecified, - server_unavailable, - cgi_error, - proxy_error, - slow_down, -}; - -enum class PermanentFailure { - unspecified, - not_found, - gone, - proxy_request_required, - bad_request, -}; - -enum class CertificateRejection { - unspecified, - not_accepted, - future_certificate_rejected, - expired_certificate_rejected, -}; - -class GeminiClient : public QObject -{ -private: - Q_OBJECT -public: - explicit GeminiClient(QObject *parent = nullptr); - - ~GeminiClient() override; - - bool startRequest(QUrl const & url); - - bool isInProgress() const; - - bool cancelRequest(); - -signals: - void requestComplete(QByteArray const & data, QString const & mime); - - void protocolViolation(QString const & reason); - - void inputRequired(QString const & query); - - void redirected(QUrl const & uri, bool is_permanent); - - void temporaryFailure(TemporaryFailure reason, QString const & info); - - void permanentFailure(PermanentFailure reason, QString const & info); - - void transientCertificateRequested(QString const & reason); - - void authorisedCertificateRequested(QString const & reason); - - void certificateRejected(CertificateRejection reason, QString const & info); - -private slots: - - void socketEncrypted(); - - void socketReadyRead(); - - void socketDisconnected(); - - void sslErrors(const QList &errors); - - void socketError(QAbstractSocket::SocketError socketError); - - -private: - bool is_receiving_body; - - QUrl target_url; - QSslSocket socket; - QByteArray buffer; - QByteArray body; - QString mime_type; -}; - -#endif // GEMINICLIENT_HPP diff --git a/geminirenderer.cpp b/geminirenderer.cpp deleted file mode 100644 index 811a946..0000000 --- a/geminirenderer.cpp +++ /dev/null @@ -1,404 +0,0 @@ -#include "geminirenderer.hpp" - -#include -#include -#include -#include -#include - -static QByteArray trim_whitespace(QByteArray items) -{ - 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); -} - -GeminiStyle::GeminiStyle() : theme(Fixed), - standard_font(), - h1_font(), - h2_font(), - h3_font(), - preformatted_font(), - background_color("#edefff"), - standard_color(0x00, 0x00, 0x00), - preformatted_color(0x00, 0x00, 0x00), - h1_color("#022f90"), - h2_color("#022f90"), - h3_color("#022f90"), - internal_link_color("#0e8fff"), - external_link_color("#0e8fff"), - cross_scheme_link_color("#0960a7"), - internal_link_prefix("→ "), - external_link_prefix("⇒ "), - margin(55.0) -{ - preformatted_font.setFamily("monospace"); - preformatted_font.setPointSizeF(10.0); - - standard_font.setFamily("sans"); - standard_font.setPointSizeF(10.0); - - h1_font.setFamily("sans"); - h1_font.setBold(true); - h1_font.setPointSizeF(20.0); - - h2_font.setFamily("sans"); - h2_font.setBold(true); - h2_font.setPointSizeF(15.0); - - h3_font.setFamily("sans"); - h3_font.setBold(true); - h3_font.setPointSizeF(12.0); -} - -bool GeminiStyle::save(QSettings &settings) const -{ - settings.beginGroup("Theme"); - - settings.setValue("standard_font", standard_font.toString()); - settings.setValue("h1_font", h1_font.toString()); - settings.setValue("h2_font", h2_font.toString()); - settings.setValue("h3_font", h3_font.toString()); - settings.setValue("preformatted_font", preformatted_font.toString()); - - settings.setValue("background_color", background_color.name()); - settings.setValue("standard_color", standard_color.name()); - settings.setValue("preformatted_color", preformatted_color.name()); - settings.setValue("h1_color", h1_color.name()); - settings.setValue("h2_color", h2_color.name()); - settings.setValue("h3_color", h3_color.name()); - settings.setValue("internal_link_color", internal_link_color.name()); - settings.setValue("external_link_color", external_link_color.name()); - settings.setValue("cross_scheme_link_color", cross_scheme_link_color.name()); - - settings.setValue("internal_link_prefix", internal_link_prefix); - settings.setValue("external_link_prefix", external_link_prefix); - - settings.setValue("margins", margin); - settings.setValue("theme", int(theme)); - - settings.endGroup(); - return true; -} - -bool GeminiStyle::load(QSettings &settings) -{ - settings.beginGroup("Theme"); - - if(settings.contains("standard_color")) - { - standard_font.fromString(settings.value("standard_font").toString()); - h1_font.fromString(settings.value("h1_font").toString()); - h2_font.fromString(settings.value("h2_font").toString()); - h3_font.fromString(settings.value("h3_font").toString()); - preformatted_font.fromString(settings.value("preformatted_font").toString()); - - background_color = QColor(settings.value("background_color").toString()); - standard_color = QColor(settings.value("standard_color").toString()); - preformatted_color = QColor(settings.value("preformatted_color").toString()); - h1_color = QColor(settings.value("h1_color").toString()); - h2_color = QColor(settings.value("h2_color").toString()); - h3_color = QColor(settings.value("h3_color").toString()); - internal_link_color = QColor(settings.value("internal_link_color").toString()); - external_link_color = QColor(settings.value("external_link_color").toString()); - cross_scheme_link_color = QColor(settings.value("cross_scheme_link_color").toString()); - - internal_link_prefix = settings.value("internal_link_prefix").toString(); - external_link_prefix = settings.value("external_link_prefix").toString(); - - margin = settings.value("margins").toDouble(); - theme = Theme(settings.value("theme").toInt()); - } - - settings.endGroup(); - return true; -} - -GeminiStyle GeminiStyle::derive(const QUrl &url) const -{ - if (this->theme == Fixed) - return *this; - - QByteArray hash = QCryptographicHash::hash(url.host().toUtf8(), QCryptographicHash::Md5); - - std::array items; - assert(items.size() == hash.size()); - memcpy(items.data(), hash.data(), items.size()); - - float hue = (items[0] + items[1]) / 510.0; - float saturation = items[2] / 255.0; - - double tmp; - GeminiStyle themed = *this; - switch (this->theme) - { - case AutoDarkTheme: - { - themed.background_color = QColor::fromHslF(hue, saturation, 0.25f); - themed.standard_color = QColor{0xFF, 0xFF, 0xFF}; - - themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); - themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); - themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); - - themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.75); - themed.internal_link_color = themed.external_link_color.lighter(110); - themed.cross_scheme_link_color = themed.external_link_color.darker(110); - - break; - } - - case AutoLightTheme: - { - themed.background_color = QColor::fromHslF(hue, items[2] / 255.0, 0.85); - themed.standard_color = QColor{0x00, 0x00, 0x00}; - - themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); - themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); - themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); - - themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.25); - themed.internal_link_color = themed.external_link_color.darker(110); - themed.cross_scheme_link_color = themed.external_link_color.lighter(110); - - break; - } - - case Fixed: - assert(false); - } - - // Same for all themes - themed.preformatted_color = themed.standard_color; - - return themed; -} - -GeminiRenderer::GeminiRenderer(GeminiStyle const &_style) : style(_style) -{ -} - -std::unique_ptr GeminiRenderer::render(const QByteArray &input, QUrl const &root_url, DocumentOutlineModel &outline) -{ - auto themed_style = style.derive(root_url); - - QTextCharFormat preformatted; - preformatted.setFont(themed_style.preformatted_font); - preformatted.setForeground(themed_style.preformatted_color); - - QTextCharFormat standard; - standard.setFont(themed_style.standard_font); - standard.setForeground(themed_style.standard_color); - - QTextCharFormat standard_link; - standard_link.setFont(themed_style.standard_font); - standard_link.setForeground(QBrush(themed_style.internal_link_color)); - - QTextCharFormat external_link; - external_link.setFont(themed_style.standard_font); - external_link.setForeground(QBrush(themed_style.external_link_color)); - - QTextCharFormat cross_protocol_link; - cross_protocol_link.setFont(themed_style.standard_font); - cross_protocol_link.setForeground(QBrush(themed_style.cross_scheme_link_color)); - - QTextCharFormat standard_h1; - standard_h1.setFont(themed_style.h1_font); - standard_h1.setForeground(QBrush(themed_style.h1_color)); - - QTextCharFormat standard_h2; - standard_h2.setFont(themed_style.h2_font); - standard_h2.setForeground(QBrush(themed_style.h2_color)); - - QTextCharFormat standard_h3; - standard_h3.setFont(themed_style.h3_font); - standard_h3.setForeground(QBrush(themed_style.h3_color)); - - std::unique_ptr result = std::make_unique(); - result->setDocumentMargin(themed_style.margin); - result->background_color = themed_style.background_color; - - QTextCursor cursor{result.get()}; - - QTextBlockFormat non_list_format = cursor.blockFormat(); - - bool verbatim = false; - QTextList *current_list = nullptr; - - outline.beginBuild(); - - int anchor_id = 0; - - auto unique_anchor_name = [&]() -> QString { - return QString("auto-title-%1").arg(++anchor_id); - }; - - QList lines = input.split('\n'); - for (auto const &line : lines) - { - if (verbatim) - { - if (line.startsWith("```")) - { - verbatim = false; - } - else - { - cursor.setCharFormat(preformatted); - cursor.insertText(line + "\n"); - } - } - else - { - if (line.startsWith("*")) - { - if (current_list == nullptr) - { - cursor.deletePreviousChar(); - current_list = cursor.insertList(QTextListFormat::ListDisc); - } - else - { - cursor.insertBlock(); - } - - QString item = trim_whitespace(line.mid(1)); - - cursor.insertText(item, standard); - continue; - } - else - { - if (current_list != nullptr) - { - cursor.insertBlock(); - cursor.setBlockFormat(non_list_format); - } - current_list = nullptr; - } - - if (line.startsWith("###")) - { - auto heading = trim_whitespace(line.mid(3)); - - auto id = unique_anchor_name(); - auto fmt = standard_h3; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); - - cursor.insertText(heading + "\n", fmt); - outline.appendH3(heading, id); - } - else if (line.startsWith("##")) - { - auto heading = trim_whitespace(line.mid(2)); - - auto id = unique_anchor_name(); - auto fmt = standard_h2; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); - - cursor.insertText(heading + "\n", fmt); - outline.appendH2(heading, id); - } - else if (line.startsWith("#")) - { - auto heading = trim_whitespace(line.mid(1)); - - auto id = unique_anchor_name(); - auto fmt = standard_h1; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); - - cursor.insertText(heading + "\n", fmt); - outline.appendH1(heading, id); - } - else if (line.startsWith("=>")) - { - auto const part = line.mid(2).trimmed(); - - QByteArray link, title; - - int index = -1; - for (int i = 0; i < part.size(); i++) - { - if (isspace(part[i])) - { - index = i; - break; - } - } - - if (index > 0) - { - link = trim_whitespace(part.mid(0, index)); - title = trim_whitespace(part.mid(index + 1)); - } - else - { - link = trim_whitespace(part); - title = trim_whitespace(part); - } - - auto local_url = QUrl(link); - - auto absolute_url = root_url.resolved(QUrl(link)); - - // qDebug() << link << title; - - auto fmt = standard_link; - - QString prefix; - if (absolute_url.host() == root_url.host()) - { - prefix = themed_style.internal_link_prefix; - fmt = standard_link; - } - else - { - prefix = themed_style.external_link_prefix; - 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()); - cursor.insertText(prefix + title + suffix + "\n", fmt); - } - else if (line.startsWith("```")) - { - verbatim = true; - } - else - { - cursor.insertText(line + "\n", standard); - } - } - } - - outline.endBuild(); - return result; -} - -GeminiDocument::GeminiDocument(QObject *parent) : QTextDocument(parent), - background_color(0x00, 0x00, 0x00) -{ -} - -GeminiDocument::~GeminiDocument() -{ -} diff --git a/geminirenderer.hpp b/geminirenderer.hpp deleted file mode 100644 index 2ec1651..0000000 --- a/geminirenderer.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef GEMINIRENDERER_HPP -#define GEMINIRENDERER_HPP - -#include -#include -#include - -#include "documentoutlinemodel.hpp" - -struct GeminiStyle -{ - enum Theme { - Fixed = 0, - AutoDarkTheme = 1, - AutoLightTheme = 2 - }; - - GeminiStyle(); - - Theme theme; - - QFont standard_font; - QFont h1_font; - QFont h2_font; - QFont h3_font; - QFont preformatted_font; - - QColor background_color; - QColor standard_color; - QColor preformatted_color; - QColor h1_color; - QColor h2_color; - QColor h3_color; - - QColor internal_link_color; - QColor external_link_color; - QColor cross_scheme_link_color; - - QString internal_link_prefix; - QString external_link_prefix; - - double margin; - - bool save(QSettings & settings) const; - bool load(QSettings & settings); - - //! Create a new style with auto-generated colors for the given - //! url. The colors are based on the host name - GeminiStyle derive(QUrl const & url) const; -}; - -class GeminiDocument : - public QTextDocument -{ - Q_OBJECT -public: - explicit GeminiDocument(QObject * parent = nullptr); - ~GeminiDocument() override; - - QColor background_color; -}; - -class GeminiRenderer -{ - GeminiStyle style; -public: - GeminiRenderer(GeminiStyle const & style = GeminiStyle{}); - - std::unique_ptr render( - QByteArray const & input, - QUrl const & root_url, - DocumentOutlineModel & outline - ); -}; - -#endif // GEMINIRENDERER_HPP diff --git a/icons.qrc b/icons.qrc deleted file mode 100644 index a0daae0..0000000 --- a/icons.qrc +++ /dev/null @@ -1,15 +0,0 @@ - - - icons/arrow-left.svg - icons/arrow-right.svg - icons/heart-outline.svg - icons/heart.svg - icons/menu.svg - icons/refresh.svg - icons/close.svg - icons/format-font.svg - icons/palette.svg - icons/kristall.svg - icons/settings.svg - - diff --git a/icons/arrow-left.svg b/icons/arrow-left.svg deleted file mode 100644 index 72f5e6d..0000000 --- a/icons/arrow-left.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/arrow-right.svg b/icons/arrow-right.svg deleted file mode 100644 index 22dc526..0000000 --- a/icons/arrow-right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/close.svg b/icons/close.svg deleted file mode 100644 index 18691d7..0000000 --- a/icons/close.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/format-font.svg b/icons/format-font.svg deleted file mode 100644 index c88cb71..0000000 --- a/icons/format-font.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/heart-outline.svg b/icons/heart-outline.svg deleted file mode 100644 index 26b2df3..0000000 --- a/icons/heart-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/heart.svg b/icons/heart.svg deleted file mode 100644 index 2cad9fc..0000000 --- a/icons/heart.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/kristall.svg b/icons/kristall.svg deleted file mode 100644 index 60be0f6..0000000 --- a/icons/kristall.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/icons/menu.svg b/icons/menu.svg deleted file mode 100644 index 64844e7..0000000 --- a/icons/menu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/palette.svg b/icons/palette.svg deleted file mode 100644 index ebf6936..0000000 --- a/icons/palette.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/refresh.svg b/icons/refresh.svg deleted file mode 100644 index ebe3f16..0000000 --- a/icons/refresh.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/settings.svg b/icons/settings.svg deleted file mode 100644 index 731a5a7..0000000 --- a/icons/settings.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/kristall.pro b/kristall.pro deleted file mode 100644 index 6aa5f60..0000000 --- a/kristall.pro +++ /dev/null @@ -1,53 +0,0 @@ -QT += core gui - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets network - -CONFIG += c++17 - -# The following define makes your compiler emit warnings if you use -# any Qt feature that has been marked deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS - -# You can also make your code fail to compile if it uses deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - browsertab.cpp \ - documentoutlinemodel.cpp \ - favouritecollection.cpp \ - geminiclient.cpp \ - geminirenderer.cpp \ - main.cpp \ - mainwindow.cpp \ - settingsdialog.cpp \ - tabbrowsinghistory.cpp - -HEADERS += \ - browsertab.hpp \ - documentoutlinemodel.hpp \ - favouritecollection.hpp \ - geminiclient.hpp \ - geminirenderer.hpp \ - mainwindow.hpp \ - settingsdialog.hpp \ - tabbrowsinghistory.hpp - -FORMS += \ - browsertab.ui \ - mainwindow.ui \ - settingsdialog.ui - -TRANSLATIONS += \ - kristall_en_US.ts - -# Default rules for deployment. -qnx: target.path = /tmp/$${TARGET}/bin -else: unix:!android: target.path = /opt/$${TARGET}/bin -!isEmpty(target.path): INSTALLS += target - -RESOURCES += \ - icons.qrc diff --git a/kristall_en_US.ts b/kristall_en_US.ts deleted file mode 100644 index ef2593b..0000000 --- a/kristall_en_US.ts +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 31cc7da..0000000 --- a/main.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "mainwindow.hpp" - -#include -#include -#include - -int main(int argc, char *argv[]) -{ - QApplication a(argc, argv); - MainWindow w; - - - - w.addNewTab(true, QUrl("gemini://gemini.circumlunar.space/")); - - w.show(); - return a.exec(); -} diff --git a/mainwindow.cpp b/mainwindow.cpp deleted file mode 100644 index 19dd922..0000000 --- a/mainwindow.cpp +++ /dev/null @@ -1,259 +0,0 @@ -#include "mainwindow.hpp" -#include "ui_mainwindow.h" -#include "browsertab.hpp" -#include "settingsdialog.hpp" - -#include -#include -#include -#include - -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent), - settings("xqTechnologies", "Kristall"), - ui(new Ui::MainWindow), - url_status(new QLabel()) -{ - ui->setupUi(this); - - this->statusBar()->addWidget(this->url_status); - - this->favourites.load(settings); - this->current_style.load(settings); - - ui->favourites_view->setModel(&favourites); - - // this->ui->history_window->setVisible(false); - this->ui->clientcert_window->setVisible(false); - this->ui->bookmarks_window->setVisible(true); - - for(QDockWidget * dock : findChildren()) - { - QAction * act = this->ui->menuView ->addAction(dock->windowTitle()); - act->setCheckable(true); - act->setChecked(dock->isVisible()); - act->setData(QVariant::fromValue(dock)); - connect(act, QOverload::of(&QAction::triggered), dock, &QDockWidget::setVisible); - } - - connect(this->ui->menuView, &QMenu::aboutToShow, [this]() { - for(QAction * act : this->ui->menuView->actions()) - { - auto * dock = qvariant_cast(act->data()); - act->setChecked(dock->isVisible()); - } - }); - - - { - settings.beginGroup("Window State"); - if(settings.contains("geometry")) { - restoreGeometry(settings.value("geometry").toByteArray()); - } - if(settings.contains("state")) { - restoreState(settings.value("state").toByteArray()); - } - settings.endGroup(); - } -} - -MainWindow::~MainWindow() -{ - - this->saveSettings(); - delete ui; -} - -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) { - this->ui->browser_tabs->setCurrentIndex(index); - } - - return tab; -} - -BrowserTab * MainWindow::addNewTab(bool focus_new, QUrl const & url) -{ - auto tab = addEmptyTab(focus_new); - tab->navigateTo(url, BrowserTab::PushImmediate); - return tab; -} - -void MainWindow::setUrlPreview(const QUrl &url) -{ - if(url.isValid()) { - auto str = url.toString(); - if(str.length() > 300) { - str = str.mid(0, 300) + "..."; - } - this->url_status->setText(str); - } - else { - this->url_status->setText(""); - } -} - -void MainWindow::saveSettings() -{ - this->favourites.save(settings); - this->current_style.save(settings); - - { - settings.beginGroup("Window State"); - - settings.setValue("geometry", saveGeometry()); - settings.setValue("state", saveState()); - - settings.endGroup(); - } -} - -void MainWindow::on_browser_tabs_currentChanged(int index) -{ - if(index >= 0) { - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->widget(index)); - - if(tab != nullptr) { - this->ui->outline_view->setModel(&tab->outline); - this->ui->outline_view->expandAll(); - - this->ui->history_view->setModel(&tab->history); - } else { - this->ui->outline_view->setModel(nullptr); - this->ui->history_view->setModel(nullptr); - } - } else { - this->ui->outline_view->setModel(nullptr); - this->ui->history_view->setModel(nullptr); - } -} - -void MainWindow::on_favourites_view_doubleClicked(const QModelIndex &index) -{ - if(auto url = this->favourites.get(index); url.isValid()) { - this->addNewTab(true, url); - } -} - -void MainWindow::on_browser_tabs_tabCloseRequested(int index) -{ - delete this->ui->browser_tabs->widget(index); -} - -void MainWindow::on_history_view_doubleClicked(const QModelIndex &index) -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - tab->navigateBack(index); - } -} - -void MainWindow::on_tab_titleChanged(const QString &title) -{ - auto * tab = qobject_cast(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(sender()); - if(tab != nullptr) { - int index = this->ui->browser_tabs->indexOf(tab); - assert(index >= 0); - this->ui->browser_tabs->setTabToolTip(index, url.toString()); - } -} - -void MainWindow::on_outline_view_clicked(const QModelIndex &index) -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - - auto anchor = tab->outline.getAnchor(index); - if(not anchor.isEmpty()) { - tab->scrollToAnchor(anchor); - } - } -} - -void MainWindow::on_actionSettings_triggered() -{ - SettingsDialog dialog; - - dialog.setGeminiStyle(this->current_style); - - if(dialog.exec() == QDialog::Accepted) { - this->current_style = dialog.geminiStyle(); - this->saveSettings(); - } -} - -void MainWindow::on_actionNew_Tab_triggered() -{ - this->addEmptyTab(true); -} - -void MainWindow::on_actionQuit_triggered() -{ - QApplication::quit(); -} - -void MainWindow::on_actionAbout_triggered() -{ - QMessageBox::about(this, - "Kristall", -R"about(Kristall, an OpenSource Gemini browser. -Made by Felix "xq" Queißner - -This is free software. You can get the source code at -https://github.com/MasterQ32/Kristall)about" - ); -} - -void MainWindow::on_actionClose_Tab_triggered() -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - delete tab; - } -} - -void MainWindow::on_actionForward_triggered() -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - tab->navOneForward(); - } -} - -void MainWindow::on_actionBackward_triggered() -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - tab->navOneBackback(); - } -} - -void MainWindow::on_actionRefresh_triggered() -{ - BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); - if(tab != nullptr) { - tab->reloadPage(); - } -} - -void MainWindow::on_actionAbout_Qt_triggered() -{ - QMessageBox::aboutQt(this, "Kristall"); -} diff --git a/mainwindow.hpp b/mainwindow.hpp deleted file mode 100644 index 41b6e2c..0000000 --- a/mainwindow.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#ifndef MAINWINDOW_HPP -#define MAINWINDOW_HPP - -#include -#include -#include - - -#include "favouritecollection.hpp" -#include "geminirenderer.hpp" - -QT_BEGIN_NAMESPACE -namespace Ui { class MainWindow; } -QT_END_NAMESPACE - -class BrowserTab; - -class MainWindow : public QMainWindow -{ - Q_OBJECT - -public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow(); - - BrowserTab * addEmptyTab(bool focus_new); - BrowserTab * addNewTab(bool focus_new, QUrl const & url); - - void setUrlPreview(QUrl const & url); - - void saveSettings(); - -public: - FavouriteCollection favourites; - -private slots: - void on_browser_tabs_currentChanged(int index); - - void on_favourites_view_doubleClicked(const QModelIndex &index); - - void on_browser_tabs_tabCloseRequested(int index); - - void on_history_view_doubleClicked(const QModelIndex &index); - - void on_tab_titleChanged(QString const & title); - - void on_tab_locationChanged(QUrl const & url); - - void on_outline_view_clicked(const QModelIndex &index); - - void on_actionSettings_triggered(); - - void on_actionNew_Tab_triggered(); - - void on_actionQuit_triggered(); - - void on_actionAbout_triggered(); - - void on_actionClose_Tab_triggered(); - - void on_actionForward_triggered(); - - void on_actionBackward_triggered(); - - void on_actionRefresh_triggered(); - - void on_actionAbout_Qt_triggered(); - -public: - QSettings settings; - GeminiStyle current_style; - -private: - Ui::MainWindow *ui; - - QLabel * url_status; -}; -#endif // MAINWINDOW_HPP diff --git a/mainwindow.ui b/mainwindow.ui deleted file mode 100644 index 08ba787..0000000 --- a/mainwindow.ui +++ /dev/null @@ -1,309 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 800 - 600 - - - - Kristall Browser - - - - :/icons/kristall.svg:/icons/kristall.svg - - - QTabWidget::Rounded - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - -1 - - - true - - - true - - - true - - - true - - - - - - - - - Document Outline - - - 1 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - false - - - - - - - - - - :/icons/heart.svg:/icons/heart.svg - - - Bookmarks - - - 2 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - History - - - 2 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - Client Certificates - - - 2 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - - - - - - - 0 - 0 - 800 - 20 - - - - - File - - - - - - - - - - - Help - - - - - - - View - - - - - Navigation - - - - - - - - - - - - - - About... - - - - - Quit - - - - - New Tab - - - Ctrl+T - - - - - Close Tab - - - Ctrl+W - - - - - - :/icons/settings.svg:/icons/settings.svg - - - Settings - - - - - - :/icons/arrow-left.svg:/icons/arrow-left.svg - - - Backward - - - Alt+Left - - - - - - :/icons/arrow-right.svg:/icons/arrow-right.svg - - - Forward - - - Alt+Right - - - - - - :/icons/refresh.svg:/icons/refresh.svg - - - Refresh - - - F5 - - - - - About Qt... - - - - - - - - diff --git a/settingsdialog.cpp b/settingsdialog.cpp deleted file mode 100644 index 2078e7c..0000000 --- a/settingsdialog.cpp +++ /dev/null @@ -1,255 +0,0 @@ -#include "settingsdialog.hpp" -#include "ui_settingsdialog.h" -#include -#include -#include - -SettingsDialog::SettingsDialog(QWidget *parent) : - QDialog(parent), - ui(new Ui::SettingsDialog), - current_style() -{ - ui->setupUi(this); - - static_assert(GeminiStyle::Fixed == 0); - static_assert(GeminiStyle::AutoDarkTheme == 1); - static_assert(GeminiStyle::AutoLightTheme == 2); - - this->ui->auto_theme->clear(); - this->ui->auto_theme->addItem("Disabled", QVariant::fromValue(GeminiStyle::Fixed)); - this->ui->auto_theme->addItem("Dark Theme", QVariant::fromValue(GeminiStyle::AutoDarkTheme)); - this->ui->auto_theme->addItem("Light Theme", QVariant::fromValue(GeminiStyle::AutoLightTheme)); - - setGeminiStyle(GeminiStyle { }); -} - -SettingsDialog::~SettingsDialog() -{ - delete ui; -} - -static QString formatFont(QFont const & font) -{ - QString style; - if(font.italic() and font.bold()) - style = "bold, italic"; - else if(font.italic()) - style = "italic"; - else if(font.bold()) - style = "bold"; - else - style = "regular"; - - return QString("%1 (%2pt, %3)") - .arg(font.family()) - .arg(font.pointSizeF()) - .arg(style); -} - -void SettingsDialog::setGeminiStyle(const GeminiStyle &style) -{ - static const QString COLOR_STYLE("border: 1px solid black; padding: 4px; background-color : %1; color : %2;"); - - this->current_style = style; - - this->ui->auto_theme->setCurrentIndex(this->current_style.theme); - - this->ui->page_margin->setValue(this->current_style.margin); - - auto setFontAndColor = [this](QLabel * label, QFont font, QColor color) - { - label->setText(formatFont(font)); - label->setStyleSheet(COLOR_STYLE - .arg(this->current_style.background_color.name()) - .arg(color.name())); - }; - - ui->bg_preview->setStyleSheet(COLOR_STYLE - .arg(this->current_style.background_color.name()) - .arg("#FF00FF")); - - ui->link_local_preview->setStyleSheet(COLOR_STYLE - .arg(this->current_style.background_color.name()) - .arg(this->current_style.internal_link_color.name())); - - ui->link_foreign_preview->setStyleSheet(COLOR_STYLE - .arg(this->current_style.background_color.name()) - .arg(this->current_style.external_link_color.name())); - - ui->link_cross_preview->setStyleSheet(COLOR_STYLE - .arg(this->current_style.background_color.name()) - .arg(this->current_style.cross_scheme_link_color.name())); - - setFontAndColor(this->ui->std_preview, this->current_style.standard_font, this->current_style.standard_color); - setFontAndColor(this->ui->pre_preview, this->current_style.preformatted_font, this->current_style.preformatted_color); - setFontAndColor(this->ui->h1_preview, this->current_style.h1_font, this->current_style.h1_color); - setFontAndColor(this->ui->h2_preview, this->current_style.h2_font, this->current_style.h2_color); - setFontAndColor(this->ui->h3_preview, this->current_style.h3_font, this->current_style.h3_color); - - this->reloadStylePreview(); -} - -void SettingsDialog::reloadStylePreview() -{ - auto const document = R"gemini(# H1 Header -## H2 Header -### H3 Header -Plain text document here. -* List A -* List B -=> rela-link Same-Site Link -=> //foreign.host/ Foreign Site Link -=> https://foreign.host/ Cross-Protocol Link -``` - ▄▄▄ ██▀███ ▄▄▄█████▓ - ▒████▄ ▓██ ▒ ██▒▓ ██▒ ▓▒ - ▒██ ▀█▄ ▓██ ░▄█ ▒▒ ▓██░ ▒░ - ░██▄▄▄▄██ ▒██▀▀█▄ ░ ▓██▓ ░ - ▓█ ▓██▒░██▓ ▒██▒ ▒██▒ ░ - ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒ ░░ - ▒ ▒▒ ░ ░▒ ░ ▒░ ░ - ░ ▒ ░░ ░ ░ - ░ ░ ░ -)gemini"; - - QString host = this->ui->preview_url->text(); - if(host.length() == 0) - host = "preview"; - - DocumentOutlineModel outline; - auto doc = GeminiRenderer { current_style }.render( - document, - QUrl(QString("about://%1/foobar").arg(host)), - outline - ); - - ui->style_preview->setStyleSheet(QString("QTextBrowser { background-color: %1; }") - .arg(doc->background_color.name())); - ui->style_preview->setDocument(doc.get()); - preview_document = std::move(doc); -} - -void SettingsDialog::updateFont(QFont & input) -{ - QFontDialog dialog { this }; - - dialog.setCurrentFont(input); - - if(dialog.exec() == QDialog::Accepted) { - input = dialog.currentFont(); - setGeminiStyle(current_style); - } -} - -void SettingsDialog::on_std_change_font_clicked() -{ - updateFont(current_style.standard_font); -} - -void SettingsDialog::on_pre_change_font_clicked() -{ - updateFont(current_style.preformatted_font); -} - -void SettingsDialog::on_h1_change_font_clicked() -{ - updateFont(current_style.h1_font); -} - -void SettingsDialog::on_h2_change_font_clicked() -{ - updateFont(current_style.h2_font); -} - -void SettingsDialog::on_h3_change_font_clicked() -{ - updateFont(current_style.h3_font); -} - -void SettingsDialog::updateColor(QColor &input) -{ - QColorDialog dialog { this }; - - dialog.setCurrentColor(input); - - if(dialog.exec() == QDialog::Accepted) { - input = dialog.currentColor(); - setGeminiStyle(current_style); - } -} - -void SettingsDialog::on_std_change_color_clicked() -{ - updateColor(current_style.standard_color); -} - -void SettingsDialog::on_pre_change_color_clicked() -{ - updateColor(current_style.preformatted_color); -} - -void SettingsDialog::on_h1_change_color_clicked() -{ - updateColor(current_style.h1_color); -} - -void SettingsDialog::on_h2_change_color_clicked() -{ - updateColor(current_style.h2_color); -} - -void SettingsDialog::on_h3_change_color_clicked() -{ - updateColor(current_style.h3_color); -} - -void SettingsDialog::on_bg_change_color_clicked() -{ - updateColor(current_style.background_color); -} - -void SettingsDialog::on_link_local_change_color_clicked() -{ - updateColor(current_style.internal_link_color); -} - -void SettingsDialog::on_link_foreign_change_color_clicked() -{ - updateColor(current_style.external_link_color); -} - -void SettingsDialog::on_link_cross_change_color_clicked() -{ - updateColor(current_style.cross_scheme_link_color); -} - -void SettingsDialog::on_link_local_prefix_textChanged(const QString &text) -{ - current_style.internal_link_prefix = text; - reloadStylePreview(); -} - -void SettingsDialog::on_link_foreign_prefix_textChanged(const QString &text) -{ - current_style.external_link_prefix = text; - reloadStylePreview(); -} - -void SettingsDialog::on_auto_theme_currentIndexChanged(int index) -{ - if(index >= 0) { - current_style.theme = GeminiStyle::Theme(index); - reloadStylePreview(); - } -} - -void SettingsDialog::on_preview_url_textChanged(const QString &arg1) -{ - this->reloadStylePreview(); -} - -void SettingsDialog::on_page_margin_valueChanged(double value) -{ - this->current_style.margin = value; - this->reloadStylePreview(); -} diff --git a/settingsdialog.hpp b/settingsdialog.hpp deleted file mode 100644 index 5f56961..0000000 --- a/settingsdialog.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef SETTINGSDIALOG_HPP -#define SETTINGSDIALOG_HPP - -#include - -#include "geminirenderer.hpp" - -namespace Ui { -class SettingsDialog; -} - -class SettingsDialog : public QDialog -{ - Q_OBJECT - -public: - explicit SettingsDialog(QWidget *parent = nullptr); - ~SettingsDialog(); - - void setGeminiStyle(GeminiStyle const & style); - - GeminiStyle geminiStyle() const { - return current_style; - } - -private slots: - void on_std_change_font_clicked(); - - void on_pre_change_font_clicked(); - - void on_h1_change_font_clicked(); - - void on_h2_change_font_clicked(); - - void on_h3_change_font_clicked(); - - void on_std_change_color_clicked(); - - void on_pre_change_color_clicked(); - - void on_h1_change_color_clicked(); - - void on_h2_change_color_clicked(); - - void on_h3_change_color_clicked(); - - void on_bg_change_color_clicked(); - - void on_link_local_change_color_clicked(); - - void on_link_foreign_change_color_clicked(); - - void on_link_cross_change_color_clicked(); - - void on_link_local_prefix_textChanged(const QString &arg1); - - void on_link_foreign_prefix_textChanged(const QString &arg1); - - void on_auto_theme_currentIndexChanged(int index); - - void on_preview_url_textChanged(const QString &arg1); - - void on_page_margin_valueChanged(double arg1); - -private: - void reloadStylePreview(); - - void updateFont(QFont & input); - - void updateColor(QColor & input); - -private: - Ui::SettingsDialog *ui; - - GeminiStyle current_style; - std::unique_ptr preview_document; -}; - -#endif // SETTINGSDIALOG_HPP diff --git a/settingsdialog.ui b/settingsdialog.ui deleted file mode 100644 index aee3959..0000000 --- a/settingsdialog.ui +++ /dev/null @@ -1,517 +0,0 @@ - - - SettingsDialog - - - - 0 - 0 - 800 - 520 - - - - Settings - - - - - - 0 - - - - Style - - - - - - 5 - - - - - - - - - - Qt::PlainText - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - Background Color - - - - - - - Standard Font - - - - - - - - - QFrame::NoFrame - - - This text will be displayed for normal text. - - - - - - - - - - - :/icons/format-font.svg:/icons/format-font.svg - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - Preformatted Font - - - - - - - - - QFrame::NoFrame - - - This text will be displayed for preformatted text. - - - - - - - - - - - :/icons/format-font.svg:/icons/format-font.svg - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - H1 Font - - - - - - - - - QFrame::NoFrame - - - This text will be displayed for a level 1 heading. - - - - - - - - - - - :/icons/format-font.svg:/icons/format-font.svg - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - H2 Font - - - - - - - - - QFrame::NoFrame - - - This text will be displayed for a level 2 heading. - - - - - - - - - - - :/icons/format-font.svg:/icons/format-font.svg - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - H3 Font - - - - - - - - - border: 1px solid black; - - - QFrame::NoFrame - - - This text will be displayed for a level 3 heading. - - - Qt::PlainText - - - - - - - - - - - :/icons/format-font.svg:/icons/format-font.svg - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - Local Link Color - - - - - - - Foreign Link Color - - - - - - - Cross-Scheme-Color - - - - - - - Local Link Prefix - - - - - - - Extern Link Prefix - - - - - - - - - - - - - - - - - - - - - - - This is a local reference - - - Qt::PlainText - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - - - This is a foreign reference - - - Qt::PlainText - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - - - This reference is cross-scheme - - - Qt::PlainText - - - - - - - - - - - :/icons/palette.svg:/icons/palette.svg - - - - - - - - - Auto-Theme Generation - - - - - - - - - - Page Margin - - - - - - - px - - - 0 - - - 350.000000000000000 - - - - - - - - - - - false - - - - - - - host.name - - - - - - - - - - Generic - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - rejected() - SettingsDialog - reject() - - - 325 - 470 - - - 286 - 274 - - - - - buttonBox - accepted() - SettingsDialog - accept() - - - 257 - 470 - - - 157 - 274 - - - - - diff --git a/src/browsertab.cpp b/src/browsertab.cpp new file mode 100644 index 0000000..6a4bf69 --- /dev/null +++ b/src/browsertab.cpp @@ -0,0 +1,405 @@ +#include "browsertab.hpp" +#include "ui_browsertab.h" +#include "mainwindow.hpp" +#include "geminirenderer.hpp" +#include "settingsdialog.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +BrowserTab::BrowserTab(MainWindow * mainWindow) : + QWidget(nullptr), + ui(new Ui::BrowserTab), + mainWindow(mainWindow), + outline(), + graphics_scene() +{ + ui->setupUi(this); + + connect(&gemini_client, &GeminiClient::requestComplete, this, &BrowserTab::on_gemini_complete); + connect(&gemini_client, &GeminiClient::protocolViolation, this, &BrowserTab::on_protocolViolation); + connect(&gemini_client, &GeminiClient::inputRequired, this, &BrowserTab::on_inputRequired); + connect(&gemini_client, &GeminiClient::redirected, this, &BrowserTab::on_redirected); + connect(&gemini_client, &GeminiClient::temporaryFailure, this, &BrowserTab::on_temporaryFailure); + connect(&gemini_client, &GeminiClient::permanentFailure, this, &BrowserTab::on_permanentFailure); + connect(&gemini_client, &GeminiClient::transientCertificateRequested, this, &BrowserTab::on_transientCertificateRequested); + connect(&gemini_client, &GeminiClient::authorisedCertificateRequested, this, &BrowserTab::on_authorisedCertificateRequested); + connect(&gemini_client, &GeminiClient::certificateRejected, this, &BrowserTab::on_certificateRejected); + + this->updateUI(); + + this->ui->graphics_browser->setVisible(false); + this->ui->text_browser->setVisible(true); + + this->ui->graphics_browser->setScene(&graphics_scene); +} + +BrowserTab::~BrowserTab() +{ + delete ui; +} + +void BrowserTab::navigateTo(const QUrl &url, PushToHistory mode) +{ + // TODO: Implement about:// scheme! + if(url.scheme() != "gemini") { + QMessageBox::warning(this, "Kristall", "Unsupported uri scheme: " + url.scheme()); + return; + } + this->current_location = url; + this->ui->url_bar->setText(url.toString()); + + if(not gemini_client.cancelRequest()) { + QMessageBox::warning(this, "Kristall", "Unsupported uri scheme: " + url.scheme()); + return; + } + + this->redirection_count = 0; + this->successfully_loaded = false; + this->push_to_history_after_load = (mode == PushAfterSuccess); + + gemini_client.startRequest(url); + + switch(mode) + { + case DontPush: + case PushAfterSuccess: + break; + + case PushImmediate: + pushToHistory(url); + break; + } + + this->updateUI(); +} + +void BrowserTab::navigateBack(QModelIndex history_index) +{ + auto url = history.get(history_index); + + if(url.isValid()) { + current_history_index = history_index; + navigateTo(url, DontPush); + } +} + +void BrowserTab::navOneBackback() +{ + navigateBack(history.oneBackward(current_history_index)); +} + +void BrowserTab::navOneForward() +{ + navigateBack(history.oneForward(current_history_index)); +} + +void BrowserTab::scrollToAnchor(QString const & anchor) +{ + qDebug() << "scroll to anchor" << anchor; + this->ui->text_browser->scrollToAnchor(anchor); +} + +void BrowserTab::reloadPage() +{ + if(current_location.isValid()) + this->navigateTo(this->current_location, DontPush); +} + +void BrowserTab::on_url_bar_returnPressed() +{ + QUrl url { this->ui->url_bar->text() }; + + if(url.scheme().isEmpty()) { + url = QUrl { "gemini://" + this->ui->url_bar->text() }; + } + + this->navigateTo(url, PushImmediate); +} + +void BrowserTab::on_refresh_button_clicked() +{ + reloadPage(); +} + +void BrowserTab::on_gemini_complete(const QByteArray &data, const QString &mime) +{ + qDebug() << "Loaded" << data.length() << "bytes of type" << mime; + + + this->graphics_scene.clear(); + this->ui->text_browser->setText(""); + + this->ui->text_browser->setVisible(mime.startsWith("text/")); + this->ui->graphics_browser->setVisible(mime.startsWith("image/")); + + ui->text_browser->setStyleSheet(""); + + std::unique_ptr document; + + this->outline.clear(); + + if(mime.startsWith("text/gemini")) { + + auto doc= GeminiRenderer{ mainWindow->current_style }.render(data, this->current_location, this->outline); + this->ui->text_browser->setStyleSheet(QString("QTextBrowser { background-color: %1; }").arg(doc->background_color.name())); + + document = std::move(doc); + } + else if(mime.startsWith("text/html")) { + document = std::make_unique(); + document->setHtml(QString::fromUtf8(data)); + } +#if defined(QT_FEATURE_textmarkdownreader) + else if(mime.startsWith("text/markdown")) { + document = std::make_unique(); + document->setMarkdown(QString::fromUtf8(data)); + } +#endif + else if(mime.startsWith("text/")) { + QFont monospace; + monospace.setFamily("monospace"); + + document = std::make_unique(); + document->setDefaultFont(monospace); + document->setPlainText(QString::fromUtf8(data)); + } + else if(mime.startsWith("image/")) { + + QImage img; + if(img.loadFromData(data, nullptr)) + { + this->graphics_scene.addPixmap(QPixmap::fromImage(img)); + } + else + { + this->graphics_scene.addText("Failed to load picture!"); + } + + this->ui->graphics_browser->fitInView(graphics_scene.sceneRect(), Qt::KeepAspectRatio); + + } + else { + this->ui->text_browser->setVisible(true); + this->ui->text_browser->setText(QString("Unsupported Mime: %1").arg(mime)); + } + + this->ui->text_browser->setDocument(document.get()); + this->current_document = std::move(document); + + emit this->locationChanged(this->current_location); + + QString title = this->current_location.toString(); + emit this->titleChanged(title); + + this->successfully_loaded = true; + + if(this->push_to_history_after_load) { + this->pushToHistory(this->current_location); + this->push_to_history_after_load = false; + } + + this->updateUI(); +} + +void BrowserTab::on_protocolViolation(const QString &reason) +{ + this->setErrorMessage(QString("Protocol violation:\n%1").arg(reason)); +} + +void BrowserTab::on_inputRequired(const QString &query) +{ + QInputDialog dialog { this }; + + dialog.setInputMode(QInputDialog::TextInput); + dialog.setLabelText(query); + + if(dialog.exec() != QDialog::Accepted) { + setErrorMessage(QString("Site requires input:\n%1").arg(query)); + return; + } + + QUrl new_location = current_location; + new_location.setQuery(dialog.textValue()); + this->navigateTo(new_location, DontPush); +} + +void BrowserTab::on_redirected(const QUrl &uri, bool is_permanent) +{ + if(redirection_count >= 5) { + setErrorMessage("Too many redirections!"); + return; + } + else { + if(gemini_client.startRequest(uri)) { + redirection_count += 1; + this->current_location = uri; + this->ui->url_bar->setText(uri.toString()); + } + } +} + +void BrowserTab::on_temporaryFailure(TemporaryFailure reason, const QString &info) +{ + switch(reason) + { + case TemporaryFailure::cgi_error: + setErrorMessage(QString("CGI Error\n%1").arg(info)); + break; + case TemporaryFailure::slow_down: + setErrorMessage(QString("Slow Down\n%1").arg(info)); + break; + case TemporaryFailure::proxy_error: + setErrorMessage(QString("Proxy Error\n%1").arg(info)); + break; + case TemporaryFailure::unspecified: + setErrorMessage(QString("Temporary Failure\n%1").arg(info)); + break; + case TemporaryFailure::server_unavailable: + setErrorMessage(QString("Server Unavailable\n%1").arg(info)); + break; + } +} + +void BrowserTab::on_permanentFailure(PermanentFailure reason, const QString &info) +{ + switch(reason) + { + case PermanentFailure::gone: + setErrorMessage(QString("Gone\n%1").arg(info)); + break; + case PermanentFailure::not_found: + setErrorMessage(QString("Not Found\n%1").arg(info)); + break; + case PermanentFailure::bad_request: + setErrorMessage(QString("Bad Request\n%1").arg(info)); + break; + case PermanentFailure::unspecified: + setErrorMessage(QString("Permanent Failure\n%1").arg(info)); + break; + case PermanentFailure::proxy_request_required: + setErrorMessage(QString("Proxy Request Required\n%1").arg(info)); + break; + } +} + +void BrowserTab::on_transientCertificateRequested(const QString &reason) +{ + QMessageBox::warning(this, "Kristall", "Transient certificate requirested:\n" + reason); + this->updateUI(); +} + +void BrowserTab::on_authorisedCertificateRequested(const QString &reason) +{ + QMessageBox::warning(this, "Kristall", "Authorized certificate requirested:\n" + reason); + this->updateUI(); +} + +void BrowserTab::on_certificateRejected(CertificateRejection reason, const QString &info) +{ + switch(reason) + { + case CertificateRejection::unspecified: + setErrorMessage(QString("Certificate Rejected\n%1").arg(info)); + break; + case CertificateRejection::not_accepted: + setErrorMessage(QString("Certificate not accepted\n%1").arg(info)); + break; + case CertificateRejection::future_certificate_rejected: + setErrorMessage(QString("Certificate is not yet valid\n%1").arg(info)); + break; + case CertificateRejection::expired_certificate_rejected: + setErrorMessage(QString("Certificate expired\n%1").arg(info)); + break; + } +} + +void BrowserTab::on_linkHovered(const QString &url) +{ + this->mainWindow->setUrlPreview(QUrl(url)); +} + +void BrowserTab::setErrorMessage(const QString &msg) +{ + // this->page.setContent(QString("An error happened:\n%0").arg(msg).toUtf8(), "text/plain charset=utf-8"); + QMessageBox::warning(this, "Kristall", msg); + this->updateUI(); +} + +void BrowserTab::pushToHistory(const QUrl &url) +{ + this->current_history_index = this->history.pushUrl(this->current_history_index, url); + this->updateUI(); +} + +void BrowserTab::on_fav_button_clicked() +{ + if(this->ui->fav_button->isChecked()) { + this->mainWindow->favourites.add(this->current_location); + } else { + this->mainWindow->favourites.remove(this->current_location); + } + + this->updateUI(); +} + + +void BrowserTab::on_text_browser_anchorClicked(const QUrl &url) +{ + qDebug() << url; + + QUrl real_url = url; + if(real_url.isRelative()) + real_url = this->current_location.resolved(url); + + if(real_url.scheme() != "gemini") { + QMessageBox::warning(this, "Kristall", QString("Unsupported url: %1").arg(real_url.toString())); + } + else { + this->navigateTo(real_url, PushAfterSuccess); + } +} + +void BrowserTab::on_text_browser_highlighted(const QUrl &url) +{ + QUrl real_url = url; + if(real_url.isRelative()) + real_url = this->current_location.resolved(url); + this->mainWindow->setUrlPreview(real_url); +} + +void BrowserTab::on_stop_button_clicked() +{ + gemini_client.cancelRequest(); +} + +void BrowserTab::on_back_button_clicked() +{ + navOneBackback(); +} + +void BrowserTab::on_forward_button_clicked() +{ + navOneForward(); +} + +void BrowserTab::updateUI() +{ + this->ui->back_button->setEnabled(history.oneBackward(current_history_index).isValid()); + this->ui->forward_button->setEnabled(history.oneForward(current_history_index).isValid()); + + this->ui->refresh_button->setVisible(this->successfully_loaded); + this->ui->stop_button->setVisible(not this->successfully_loaded); + + this->ui->fav_button->setEnabled(this->successfully_loaded); + this->ui->fav_button->setChecked(this->mainWindow->favourites.contains(this->current_location)); +} diff --git a/src/browsertab.hpp b/src/browsertab.hpp new file mode 100644 index 0000000..7f0ac35 --- /dev/null +++ b/src/browsertab.hpp @@ -0,0 +1,114 @@ +#ifndef BROWSERTAB_HPP +#define BROWSERTAB_HPP + +#include +#include +#include +#include + +#include "geminiclient.hpp" +#include "documentoutlinemodel.hpp" +#include "tabbrowsinghistory.hpp" +#include "geminirenderer.hpp" + +namespace Ui { +class BrowserTab; +} + +class MainWindow; + +class BrowserTab : public QWidget +{ + Q_OBJECT +public: + enum PushToHistory { + DontPush, + PushImmediate, + PushAfterSuccess, + }; + +public: + explicit BrowserTab(MainWindow * mainWindow); + ~BrowserTab(); + + void navigateTo(QUrl const & url, PushToHistory mode); + + void navigateBack(QModelIndex history_index); + + void navOneBackback(); + + void navOneForward(); + + void scrollToAnchor(QString const & anchor); + + void reloadPage(); + +signals: + void titleChanged(QString const & title); + void locationChanged(QUrl const & url); + +private slots: + void on_url_bar_returnPressed(); + + void on_refresh_button_clicked(); + + void on_gemini_complete(QByteArray const & data, QString const & mime); + + void on_protocolViolation(QString const & reason); + + void on_inputRequired(QString const & query); + + void on_redirected(QUrl const & uri, bool is_permanent); + + void on_temporaryFailure(TemporaryFailure reason, QString const & info); + + void on_permanentFailure(PermanentFailure reason, QString const & info); + + void on_transientCertificateRequested(QString const & reason); + + void on_authorisedCertificateRequested(QString const & reason); + + void on_certificateRejected(CertificateRejection reason, QString const & info); + + void on_linkHovered(const QString &url); + + void on_fav_button_clicked(); + + void on_text_browser_anchorClicked(const QUrl &arg1); + + void on_text_browser_highlighted(const QUrl &arg1); + + void on_back_button_clicked(); + + void on_forward_button_clicked(); + + void on_stop_button_clicked(); + +private: + void setErrorMessage(QString const & msg); + + void pushToHistory(QUrl const & url); + + void updateUI(); + +public: + + Ui::BrowserTab *ui; + MainWindow * mainWindow; + QUrl current_location; + + GeminiClient gemini_client; + int redirection_count = 0; + + bool push_to_history_after_load = false; + bool successfully_loaded = false; + + DocumentOutlineModel outline; + QGraphicsScene graphics_scene; + TabBrowsingHistory history; + QModelIndex current_history_index; + + std::unique_ptr current_document; +}; + +#endif // BROWSERTAB_HPP diff --git a/src/browsertab.ui b/src/browsertab.ui new file mode 100644 index 0000000..265560e --- /dev/null +++ b/src/browsertab.ui @@ -0,0 +1,187 @@ + + + BrowserTab + + + + 0 + 0 + 692 + 404 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QLayout::SetDefaultConstraint + + + 9 + + + 9 + + + 9 + + + 9 + + + + + false + + + + + + + :/icons/arrow-left.svg:/icons/arrow-left.svg + + + + + + + false + + + ... + + + + :/icons/arrow-right.svg:/icons/arrow-right.svg + + + + + + + true + + + + + + + :/icons/close.svg:/icons/close.svg + + + + + + + true + + + + + + + :/icons/refresh.svg:/icons/refresh.svg + + + + + + + gemini:// + + + + + + + false + + + + + + + :/icons/heart-outline.svg + :/icons/heart.svg:/icons/heart-outline.svg + + + true + + + false + + + + + + + + + QLayout::SetDefaultConstraint + + + + + + + + QTextEdit::AutoNone + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Start surfin!</p></body></html> + + + 40 + + + false + + + + + + + QGraphicsView::ScrollHandDrag + + + QGraphicsView::AnchorUnderMouse + + + QGraphicsView::AnchorUnderMouse + + + + + + + + + + + + diff --git a/src/documentoutlinemodel.cpp b/src/documentoutlinemodel.cpp new file mode 100644 index 0000000..5978f33 --- /dev/null +++ b/src/documentoutlinemodel.cpp @@ -0,0 +1,200 @@ +#include "documentoutlinemodel.hpp" + +#include + +DocumentOutlineModel::DocumentOutlineModel() : + QAbstractItemModel(), + root() +{ + +} + +void DocumentOutlineModel::clear() +{ + beginBuild(); + endBuild(); + +} + +void DocumentOutlineModel::beginBuild() +{ + beginResetModel(); + root = Node { + nullptr, + "", "", + 0, 0, + QList { }, + }; +} + +void DocumentOutlineModel::appendH1(const QString &title, QString const & anchor) +{ + root.children.append(Node { + &root, + title, anchor, + 1, 0, + QList { }, + }); +} + +void DocumentOutlineModel::appendH2(const QString &title, QString const & anchor) +{ + auto & parent = ensureLevel1(); + parent.children.append(Node { + &parent, + title, anchor, + 2, parent.children.size() - 1, + QList { }, + }); +} + +void DocumentOutlineModel::appendH3(const QString &title, QString const & anchor) +{ + auto & parent = ensureLevel2(); + parent.children.append(Node { + &parent, + title, anchor, + 3, parent.children.size() - 1, + QList { }, + }); +} + +void DocumentOutlineModel::endBuild() +{ + for(auto const & h1 : this->root.children) + { + assert(h1.depth == 1); + assert(h1.parent == &this->root); + for(auto const & h2 : h1.children) + { + assert(h2.depth == 2); + assert(h2.parent == &h1); + for(auto const & h3 : h2.children) + { + assert(h3.depth == 3); + assert(h3.parent == &h2); + } + } + } + endResetModel(); +} + +QString DocumentOutlineModel::getTitle(const QModelIndex &index) const +{ + if(not index.isValid()) + return ""; + + Node const *childItem = static_cast(index.internalPointer()); + + return childItem->title; +} + +QString DocumentOutlineModel::getAnchor(const QModelIndex &index) const +{ + if(not index.isValid()) + return ""; + + Node const *childItem = static_cast(index.internalPointer()); + + return childItem->anchor; +} + +QModelIndex DocumentOutlineModel::index(int row, int column, const QModelIndex &parent) const +{ + if (not hasIndex(row, column, parent)) + return QModelIndex(); + + Node const * parentItem; + + if (!parent.isValid()) + parentItem = &this->root; + else + parentItem = static_cast(parent.internalPointer()); + + Node const * childItem = &parentItem->children[row]; + if (childItem) + return createIndex(row, column, reinterpret_cast(childItem)); + return QModelIndex(); + +} + +QModelIndex DocumentOutlineModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) + return QModelIndex(); + + Node const *childItem = static_cast(child.internalPointer()); + Node const * parent = childItem->parent; + + if (parent == &root) + return QModelIndex(); + + return createIndex( + parent->index, + 0, + reinterpret_cast(parent)); +} + +int DocumentOutlineModel::rowCount(const QModelIndex &parent) const +{ + Node const *parentItem; + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = &root; + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->children.size(); +} + +int DocumentOutlineModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant DocumentOutlineModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.column() != 0) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + Node const *item = static_cast(index.internalPointer()); + + return item->title; +} + +DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel1() +{ + if(root.children.size() == 0) { + root.children.append(Node { + &root, + "", "", + 1, 0, + QList { }, + }); + } + return root.children.last(); +} + +DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel2() +{ + auto & parent = ensureLevel1(); + + if(parent.children.size() == 0) { + root.children.append(Node { + &parent, + "", "", + 2, 0, + QList { }, + }); + } + + return parent.children.last(); +} diff --git a/src/documentoutlinemodel.hpp b/src/documentoutlinemodel.hpp new file mode 100644 index 0000000..0476892 --- /dev/null +++ b/src/documentoutlinemodel.hpp @@ -0,0 +1,57 @@ +#ifndef DOCUMENTOUTLINEMODEL_HPP +#define DOCUMENTOUTLINEMODEL_HPP + +#include +#include + +class DocumentOutlineModel : + public QAbstractItemModel +{ + Q_OBJECT +public: + DocumentOutlineModel(); + + void clear(); + + void beginBuild(); + + void appendH1(QString const & title, QString const & anchor); + + void appendH2(QString const & title, QString const & anchor); + + void appendH3(QString const & title, QString const & anchor); + + void endBuild(); + + QString getTitle(QModelIndex const & index) const; + QString getAnchor(QModelIndex const & index) const; + +public: + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex parent(const QModelIndex &child) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + +private: + struct Node + { + Node * parent; + QString title; + QString anchor; + int depth = 0; + int index = 0; + QList children; + }; + + Node root; + + Node & ensureLevel1(); + Node & ensureLevel2(); +}; + +#endif // DOCUMENTOUTLINEMODEL_HPP diff --git a/src/favouritecollection.cpp b/src/favouritecollection.cpp new file mode 100644 index 0000000..161eacf --- /dev/null +++ b/src/favouritecollection.cpp @@ -0,0 +1,140 @@ +#include "favouritecollection.hpp" + +#include + +FavouriteCollection::FavouriteCollection(QObject *parent) : + QAbstractListModel(parent) +{ + +} + +void FavouriteCollection::add(QUrl const & url) +{ + if(contains(url)) + return; + + beginInsertRows(QModelIndex{}, items.size(), items.size() + 1); + items.push_back(url); + endInsertRows(); +} + +void FavouriteCollection::remove(QUrl const & url) +{ + for(int i = 0; i < items.size(); i++) + { + if(items.at(i) == url) { + beginRemoveRows(QModelIndex{}, i, i + 1); + items.removeAt(i); + endRemoveRows(); + return; + } + } +} + +bool FavouriteCollection::contains(const QUrl &url) +{ + for(auto const & item : items) { + if(item == url) + return true; + } + return false; +} + +QUrl FavouriteCollection::get(const QModelIndex &index) const +{ + if(index.isValid()) { + return items.at(index.row()); + } else { + return QUrl { }; + } +} + +bool FavouriteCollection::save(const QString &fileName) const +{ + QFile file(fileName); + if(not file.open(QFile::WriteOnly)) + return false; + + for(auto const & url: items) + { + QByteArray blob = (url.toString() + "\n").toUtf8(); + + qint64 offset = 0; + while(offset < blob.size()) + { + auto len = file.write(blob.data() + offset, blob.size() - offset); + if(len <= 0) { + file.close(); + return false; + } + offset += len; + } + } + + file.close(); + return true; +} + +bool FavouriteCollection::save(QSettings &settings) const +{ + settings.beginWriteArray("favourites", items.size()); + for(int i = 0; i < items.size(); i++) + { + settings.setArrayIndex(i); + settings.setValue("url", items[i].toString()); + } + settings.endArray(); + return true; +} + +bool FavouriteCollection::load(const QString &fileName) +{ + QFile file(fileName); + if(not file.open(QFile::ReadOnly)) + return false; + auto data = file.readAll(); + + beginResetModel(); + + items.clear(); + for(auto line : data.split('\n')) { + if(line.size() > 0) { + items.push_back(QUrl(QString::fromUtf8(line))); + } + } + endResetModel(); + + return true; +} + +bool FavouriteCollection::load(QSettings & settings) +{ + int len = settings.beginReadArray("favourites"); + items.resize(len); + for(int i = 0; i < items.size(); i++) + { + settings.setArrayIndex(i); + items[i] = settings.value("url").toString(); + } + settings.endArray(); + return true; +} + +int FavouriteCollection::rowCount(const QModelIndex &parent) const +{ + return items.size(); +} + +bool FavouriteCollection::setData(const QModelIndex &index, const QVariant &value, int role) +{ + return false; +} + +QVariant FavouriteCollection::data(const QModelIndex &index, int role) const +{ + if(role != Qt::DisplayRole) { + return QVariant{}; + } + return items.at(index.row()).toString(); +} + diff --git a/src/favouritecollection.hpp b/src/favouritecollection.hpp new file mode 100644 index 0000000..73afa1f --- /dev/null +++ b/src/favouritecollection.hpp @@ -0,0 +1,44 @@ +#ifndef FAVOURITECOLLECTION_HPP +#define FAVOURITECOLLECTION_HPP + +#include +#include +#include +#include + + +class FavouriteCollection : public QAbstractListModel +{ + Q_OBJECT +public: + explicit FavouriteCollection(QObject *parent = nullptr); + + void add(QUrl const & url); + + void remove(QUrl const & url); + + bool contains(QUrl const & url); + + QUrl get(QModelIndex const & index) const ; + + bool save(QString const & fileName) const; + bool save(QSettings & settings) const; + + bool load(QString const & fileName); + bool load(QSettings & settings); + +public: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +signals: + +private: + QVector items; + +}; + +#endif // FAVOURITECOLLECTION_HPP diff --git a/src/geminiclient.cpp b/src/geminiclient.cpp new file mode 100644 index 0000000..44fa864 --- /dev/null +++ b/src/geminiclient.cpp @@ -0,0 +1,252 @@ +#include "geminiclient.hpp" + +#include + +GeminiClient::GeminiClient(QObject *parent) : QObject(parent) +{ + connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); + connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); + connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); + connect(&socket, QOverload &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); + connect(&socket, QOverload::of(&QSslSocket::error), this, &GeminiClient::socketError); +} + +GeminiClient::~GeminiClient() +{ + is_receiving_body = false; +} + +bool GeminiClient::startRequest(const QUrl &url) +{ + if(socket.isOpen()) + return false; + + socket.connectToHostEncrypted(url.host(), url.port(1965)); + + buffer.clear(); + body.clear(); + is_receiving_body = false; + + if(not socket.isOpen()) + return false; + + target_url = url; + mime_type = ""; + + return true; +} + +bool GeminiClient::isInProgress() const +{ + return socket.isOpen(); +} + +bool GeminiClient::cancelRequest() +{ + this->is_receiving_body = false; + this->socket.close(); + this->buffer.clear(); + this->body.clear(); + return true; +} + +void GeminiClient::socketEncrypted() +{ + QString request = target_url.toString() + "\r\n"; + + QByteArray request_bytes = request.toUtf8(); + + qint64 offset = 0; + while(offset < request_bytes.size()) { + auto const len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset); + if(len <= 0) + { + socket.close(); + return; + } + offset += len; + } +} + +void GeminiClient::socketReadyRead() +{ + QByteArray response = socket.readAll(); + + if(is_receiving_body) + { + body.append(response); + } + else + { + for(int i = 0; i < response.size(); i++) + { + if(response[i] == '\n') { + buffer.append(response.data(), i); + body.append(response.data() + i + 1, response.size() - i - 1); + + // "XY " + if(buffer.size() <= 5) { + socket.close(); + qDebug() << buffer; + emit protocolViolation("Line is too short for valid protocol"); + return; + } + if(buffer[buffer.size() - 1] != '\r') { + socket.close(); + qDebug() << buffer; + emit protocolViolation("Line does not end with "); + return; + } + if(not isdigit(buffer[0])) { + socket.close(); + qDebug() << buffer; + emit protocolViolation("First character is not a digit."); + return; + } + if(not isdigit(buffer[1])) { + socket.close(); + qDebug() << buffer; + emit protocolViolation("Second character is not a digit."); + return; + } + // TODO: Implement stricter version + // if(buffer[2] != ' ') { + if(not isspace(buffer[2])) { + socket.close(); + qDebug() << buffer; + emit protocolViolation("Third character is not a space."); + return; + } + + QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4); + + int primary_code = buffer[0] - '0'; + int secondary_code = buffer[1] - '0'; + + qDebug() << primary_code << secondary_code << meta; + + // We don't need to receive any data after that. + if(primary_code != 2) + socket.close(); + + switch(primary_code) + { + case 1: // requesting input + emit inputRequired(meta); + return; + + case 2: // success + is_receiving_body = true; + mime_type = meta; + return; + + case 3: { // redirect + QUrl new_url(meta); + if(new_url.isValid()) { + if(new_url.isRelative()) + new_url = target_url.resolved(new_url); + assert(not new_url.isRelative()); + + emit redirected(new_url, (secondary_code == 1)); + } + else { + emit protocolViolation("Invalid URL for redirection!"); + } + return; + } + + case 4: { // temporary failure + TemporaryFailure type = TemporaryFailure::unspecified; + switch(secondary_code) + { + case 1: type = TemporaryFailure::server_unavailable; break; + case 2: type = TemporaryFailure::cgi_error; break; + case 3: type = TemporaryFailure::proxy_error; break; + case 4: type = TemporaryFailure::slow_down; break; + } + emit temporaryFailure(type, meta); + return; + } + + case 5: { // permanent failure + PermanentFailure type = PermanentFailure::unspecified; + switch(secondary_code) + { + case 1: type = PermanentFailure::not_found; break; + case 2: type = PermanentFailure::gone; break; + case 3: type = PermanentFailure::proxy_request_required; break; + case 9: type = PermanentFailure::bad_request; break; + } + emit permanentFailure(type, meta); + return; + } + + case 6: // client certificate required + switch(secondary_code) + { + case 1: + emit transientCertificateRequested(meta); + return; + + case 2: + emit authorisedCertificateRequested(meta); + return; + + case 3: + emit certificateRejected(CertificateRejection::not_accepted, meta); + return; + + case 4: + emit certificateRejected(CertificateRejection::future_certificate_rejected, meta); + return; + + case 5: + emit certificateRejected(CertificateRejection::expired_certificate_rejected, meta); + return; + + default: + emit certificateRejected(CertificateRejection::unspecified, meta); + return; + } + return; + + default: + emit protocolViolation("Unspecified status code used!"); + return; + } + + assert(false and "unreachable"); + } + } + buffer.append(response); + } +} + +void GeminiClient::socketDisconnected() +{ + if(is_receiving_body) { + body.append(socket.readAll()); + emit requestComplete(body, mime_type); + } +} + +void GeminiClient::sslErrors(const QList &errors) +{ + for(auto const & error : errors) { + qWarning() << error.errorString() ; + } + + socket.ignoreSslErrors(errors); +} + +void GeminiClient::socketError(QAbstractSocket::SocketError socketError) +{ + // When remote host closes TLS session, the client closes the socket. + // This is more sane then erroring out here as it's a perfectly legal + // state and we know the TLS connection has ended. + if(socketError == QAbstractSocket::RemoteHostClosedError) { + socket.close(); + } else { + qWarning() << socketError << socket.errorString(); + } +} diff --git a/src/geminiclient.hpp b/src/geminiclient.hpp new file mode 100644 index 0000000..590eb5b --- /dev/null +++ b/src/geminiclient.hpp @@ -0,0 +1,89 @@ +#ifndef GEMINICLIENT_HPP +#define GEMINICLIENT_HPP + +#include +#include +#include +#include + +enum class TemporaryFailure { + unspecified, + server_unavailable, + cgi_error, + proxy_error, + slow_down, +}; + +enum class PermanentFailure { + unspecified, + not_found, + gone, + proxy_request_required, + bad_request, +}; + +enum class CertificateRejection { + unspecified, + not_accepted, + future_certificate_rejected, + expired_certificate_rejected, +}; + +class GeminiClient : public QObject +{ +private: + Q_OBJECT +public: + explicit GeminiClient(QObject *parent = nullptr); + + ~GeminiClient() override; + + bool startRequest(QUrl const & url); + + bool isInProgress() const; + + bool cancelRequest(); + +signals: + void requestComplete(QByteArray const & data, QString const & mime); + + void protocolViolation(QString const & reason); + + void inputRequired(QString const & query); + + void redirected(QUrl const & uri, bool is_permanent); + + void temporaryFailure(TemporaryFailure reason, QString const & info); + + void permanentFailure(PermanentFailure reason, QString const & info); + + void transientCertificateRequested(QString const & reason); + + void authorisedCertificateRequested(QString const & reason); + + void certificateRejected(CertificateRejection reason, QString const & info); + +private slots: + + void socketEncrypted(); + + void socketReadyRead(); + + void socketDisconnected(); + + void sslErrors(const QList &errors); + + void socketError(QAbstractSocket::SocketError socketError); + + +private: + bool is_receiving_body; + + QUrl target_url; + QSslSocket socket; + QByteArray buffer; + QByteArray body; + QString mime_type; +}; + +#endif // GEMINICLIENT_HPP diff --git a/src/geminirenderer.cpp b/src/geminirenderer.cpp new file mode 100644 index 0000000..811a946 --- /dev/null +++ b/src/geminirenderer.cpp @@ -0,0 +1,404 @@ +#include "geminirenderer.hpp" + +#include +#include +#include +#include +#include + +static QByteArray trim_whitespace(QByteArray items) +{ + 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); +} + +GeminiStyle::GeminiStyle() : theme(Fixed), + standard_font(), + h1_font(), + h2_font(), + h3_font(), + preformatted_font(), + background_color("#edefff"), + standard_color(0x00, 0x00, 0x00), + preformatted_color(0x00, 0x00, 0x00), + h1_color("#022f90"), + h2_color("#022f90"), + h3_color("#022f90"), + internal_link_color("#0e8fff"), + external_link_color("#0e8fff"), + cross_scheme_link_color("#0960a7"), + internal_link_prefix("→ "), + external_link_prefix("⇒ "), + margin(55.0) +{ + preformatted_font.setFamily("monospace"); + preformatted_font.setPointSizeF(10.0); + + standard_font.setFamily("sans"); + standard_font.setPointSizeF(10.0); + + h1_font.setFamily("sans"); + h1_font.setBold(true); + h1_font.setPointSizeF(20.0); + + h2_font.setFamily("sans"); + h2_font.setBold(true); + h2_font.setPointSizeF(15.0); + + h3_font.setFamily("sans"); + h3_font.setBold(true); + h3_font.setPointSizeF(12.0); +} + +bool GeminiStyle::save(QSettings &settings) const +{ + settings.beginGroup("Theme"); + + settings.setValue("standard_font", standard_font.toString()); + settings.setValue("h1_font", h1_font.toString()); + settings.setValue("h2_font", h2_font.toString()); + settings.setValue("h3_font", h3_font.toString()); + settings.setValue("preformatted_font", preformatted_font.toString()); + + settings.setValue("background_color", background_color.name()); + settings.setValue("standard_color", standard_color.name()); + settings.setValue("preformatted_color", preformatted_color.name()); + settings.setValue("h1_color", h1_color.name()); + settings.setValue("h2_color", h2_color.name()); + settings.setValue("h3_color", h3_color.name()); + settings.setValue("internal_link_color", internal_link_color.name()); + settings.setValue("external_link_color", external_link_color.name()); + settings.setValue("cross_scheme_link_color", cross_scheme_link_color.name()); + + settings.setValue("internal_link_prefix", internal_link_prefix); + settings.setValue("external_link_prefix", external_link_prefix); + + settings.setValue("margins", margin); + settings.setValue("theme", int(theme)); + + settings.endGroup(); + return true; +} + +bool GeminiStyle::load(QSettings &settings) +{ + settings.beginGroup("Theme"); + + if(settings.contains("standard_color")) + { + standard_font.fromString(settings.value("standard_font").toString()); + h1_font.fromString(settings.value("h1_font").toString()); + h2_font.fromString(settings.value("h2_font").toString()); + h3_font.fromString(settings.value("h3_font").toString()); + preformatted_font.fromString(settings.value("preformatted_font").toString()); + + background_color = QColor(settings.value("background_color").toString()); + standard_color = QColor(settings.value("standard_color").toString()); + preformatted_color = QColor(settings.value("preformatted_color").toString()); + h1_color = QColor(settings.value("h1_color").toString()); + h2_color = QColor(settings.value("h2_color").toString()); + h3_color = QColor(settings.value("h3_color").toString()); + internal_link_color = QColor(settings.value("internal_link_color").toString()); + external_link_color = QColor(settings.value("external_link_color").toString()); + cross_scheme_link_color = QColor(settings.value("cross_scheme_link_color").toString()); + + internal_link_prefix = settings.value("internal_link_prefix").toString(); + external_link_prefix = settings.value("external_link_prefix").toString(); + + margin = settings.value("margins").toDouble(); + theme = Theme(settings.value("theme").toInt()); + } + + settings.endGroup(); + return true; +} + +GeminiStyle GeminiStyle::derive(const QUrl &url) const +{ + if (this->theme == Fixed) + return *this; + + QByteArray hash = QCryptographicHash::hash(url.host().toUtf8(), QCryptographicHash::Md5); + + std::array items; + assert(items.size() == hash.size()); + memcpy(items.data(), hash.data(), items.size()); + + float hue = (items[0] + items[1]) / 510.0; + float saturation = items[2] / 255.0; + + double tmp; + GeminiStyle themed = *this; + switch (this->theme) + { + case AutoDarkTheme: + { + themed.background_color = QColor::fromHslF(hue, saturation, 0.25f); + themed.standard_color = QColor{0xFF, 0xFF, 0xFF}; + + themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); + themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); + themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75); + + themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.75); + themed.internal_link_color = themed.external_link_color.lighter(110); + themed.cross_scheme_link_color = themed.external_link_color.darker(110); + + break; + } + + case AutoLightTheme: + { + themed.background_color = QColor::fromHslF(hue, items[2] / 255.0, 0.85); + themed.standard_color = QColor{0x00, 0x00, 0x00}; + + themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); + themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); + themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25); + + themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.25); + themed.internal_link_color = themed.external_link_color.darker(110); + themed.cross_scheme_link_color = themed.external_link_color.lighter(110); + + break; + } + + case Fixed: + assert(false); + } + + // Same for all themes + themed.preformatted_color = themed.standard_color; + + return themed; +} + +GeminiRenderer::GeminiRenderer(GeminiStyle const &_style) : style(_style) +{ +} + +std::unique_ptr GeminiRenderer::render(const QByteArray &input, QUrl const &root_url, DocumentOutlineModel &outline) +{ + auto themed_style = style.derive(root_url); + + QTextCharFormat preformatted; + preformatted.setFont(themed_style.preformatted_font); + preformatted.setForeground(themed_style.preformatted_color); + + QTextCharFormat standard; + standard.setFont(themed_style.standard_font); + standard.setForeground(themed_style.standard_color); + + QTextCharFormat standard_link; + standard_link.setFont(themed_style.standard_font); + standard_link.setForeground(QBrush(themed_style.internal_link_color)); + + QTextCharFormat external_link; + external_link.setFont(themed_style.standard_font); + external_link.setForeground(QBrush(themed_style.external_link_color)); + + QTextCharFormat cross_protocol_link; + cross_protocol_link.setFont(themed_style.standard_font); + cross_protocol_link.setForeground(QBrush(themed_style.cross_scheme_link_color)); + + QTextCharFormat standard_h1; + standard_h1.setFont(themed_style.h1_font); + standard_h1.setForeground(QBrush(themed_style.h1_color)); + + QTextCharFormat standard_h2; + standard_h2.setFont(themed_style.h2_font); + standard_h2.setForeground(QBrush(themed_style.h2_color)); + + QTextCharFormat standard_h3; + standard_h3.setFont(themed_style.h3_font); + standard_h3.setForeground(QBrush(themed_style.h3_color)); + + std::unique_ptr result = std::make_unique(); + result->setDocumentMargin(themed_style.margin); + result->background_color = themed_style.background_color; + + QTextCursor cursor{result.get()}; + + QTextBlockFormat non_list_format = cursor.blockFormat(); + + bool verbatim = false; + QTextList *current_list = nullptr; + + outline.beginBuild(); + + int anchor_id = 0; + + auto unique_anchor_name = [&]() -> QString { + return QString("auto-title-%1").arg(++anchor_id); + }; + + QList lines = input.split('\n'); + for (auto const &line : lines) + { + if (verbatim) + { + if (line.startsWith("```")) + { + verbatim = false; + } + else + { + cursor.setCharFormat(preformatted); + cursor.insertText(line + "\n"); + } + } + else + { + if (line.startsWith("*")) + { + if (current_list == nullptr) + { + cursor.deletePreviousChar(); + current_list = cursor.insertList(QTextListFormat::ListDisc); + } + else + { + cursor.insertBlock(); + } + + QString item = trim_whitespace(line.mid(1)); + + cursor.insertText(item, standard); + continue; + } + else + { + if (current_list != nullptr) + { + cursor.insertBlock(); + cursor.setBlockFormat(non_list_format); + } + current_list = nullptr; + } + + if (line.startsWith("###")) + { + auto heading = trim_whitespace(line.mid(3)); + + auto id = unique_anchor_name(); + auto fmt = standard_h3; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); + + cursor.insertText(heading + "\n", fmt); + outline.appendH3(heading, id); + } + else if (line.startsWith("##")) + { + auto heading = trim_whitespace(line.mid(2)); + + auto id = unique_anchor_name(); + auto fmt = standard_h2; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); + + cursor.insertText(heading + "\n", fmt); + outline.appendH2(heading, id); + } + else if (line.startsWith("#")) + { + auto heading = trim_whitespace(line.mid(1)); + + auto id = unique_anchor_name(); + auto fmt = standard_h1; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); + + cursor.insertText(heading + "\n", fmt); + outline.appendH1(heading, id); + } + else if (line.startsWith("=>")) + { + auto const part = line.mid(2).trimmed(); + + QByteArray link, title; + + int index = -1; + for (int i = 0; i < part.size(); i++) + { + if (isspace(part[i])) + { + index = i; + break; + } + } + + if (index > 0) + { + link = trim_whitespace(part.mid(0, index)); + title = trim_whitespace(part.mid(index + 1)); + } + else + { + link = trim_whitespace(part); + title = trim_whitespace(part); + } + + auto local_url = QUrl(link); + + auto absolute_url = root_url.resolved(QUrl(link)); + + // qDebug() << link << title; + + auto fmt = standard_link; + + QString prefix; + if (absolute_url.host() == root_url.host()) + { + prefix = themed_style.internal_link_prefix; + fmt = standard_link; + } + else + { + prefix = themed_style.external_link_prefix; + 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()); + cursor.insertText(prefix + title + suffix + "\n", fmt); + } + else if (line.startsWith("```")) + { + verbatim = true; + } + else + { + cursor.insertText(line + "\n", standard); + } + } + } + + outline.endBuild(); + return result; +} + +GeminiDocument::GeminiDocument(QObject *parent) : QTextDocument(parent), + background_color(0x00, 0x00, 0x00) +{ +} + +GeminiDocument::~GeminiDocument() +{ +} diff --git a/src/geminirenderer.hpp b/src/geminirenderer.hpp new file mode 100644 index 0000000..2ec1651 --- /dev/null +++ b/src/geminirenderer.hpp @@ -0,0 +1,76 @@ +#ifndef GEMINIRENDERER_HPP +#define GEMINIRENDERER_HPP + +#include +#include +#include + +#include "documentoutlinemodel.hpp" + +struct GeminiStyle +{ + enum Theme { + Fixed = 0, + AutoDarkTheme = 1, + AutoLightTheme = 2 + }; + + GeminiStyle(); + + Theme theme; + + QFont standard_font; + QFont h1_font; + QFont h2_font; + QFont h3_font; + QFont preformatted_font; + + QColor background_color; + QColor standard_color; + QColor preformatted_color; + QColor h1_color; + QColor h2_color; + QColor h3_color; + + QColor internal_link_color; + QColor external_link_color; + QColor cross_scheme_link_color; + + QString internal_link_prefix; + QString external_link_prefix; + + double margin; + + bool save(QSettings & settings) const; + bool load(QSettings & settings); + + //! Create a new style with auto-generated colors for the given + //! url. The colors are based on the host name + GeminiStyle derive(QUrl const & url) const; +}; + +class GeminiDocument : + public QTextDocument +{ + Q_OBJECT +public: + explicit GeminiDocument(QObject * parent = nullptr); + ~GeminiDocument() override; + + QColor background_color; +}; + +class GeminiRenderer +{ + GeminiStyle style; +public: + GeminiRenderer(GeminiStyle const & style = GeminiStyle{}); + + std::unique_ptr render( + QByteArray const & input, + QUrl const & root_url, + DocumentOutlineModel & outline + ); +}; + +#endif // GEMINIRENDERER_HPP diff --git a/src/icons.qrc b/src/icons.qrc new file mode 100644 index 0000000..a0daae0 --- /dev/null +++ b/src/icons.qrc @@ -0,0 +1,15 @@ + + + icons/arrow-left.svg + icons/arrow-right.svg + icons/heart-outline.svg + icons/heart.svg + icons/menu.svg + icons/refresh.svg + icons/close.svg + icons/format-font.svg + icons/palette.svg + icons/kristall.svg + icons/settings.svg + + diff --git a/src/icons/arrow-left.svg b/src/icons/arrow-left.svg new file mode 100644 index 0000000..72f5e6d --- /dev/null +++ b/src/icons/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/arrow-right.svg b/src/icons/arrow-right.svg new file mode 100644 index 0000000..22dc526 --- /dev/null +++ b/src/icons/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/close.svg b/src/icons/close.svg new file mode 100644 index 0000000..18691d7 --- /dev/null +++ b/src/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/format-font.svg b/src/icons/format-font.svg new file mode 100644 index 0000000..c88cb71 --- /dev/null +++ b/src/icons/format-font.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/heart-outline.svg b/src/icons/heart-outline.svg new file mode 100644 index 0000000..26b2df3 --- /dev/null +++ b/src/icons/heart-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/heart.svg b/src/icons/heart.svg new file mode 100644 index 0000000..2cad9fc --- /dev/null +++ b/src/icons/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/kristall.svg b/src/icons/kristall.svg new file mode 100644 index 0000000..60be0f6 --- /dev/null +++ b/src/icons/kristall.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/src/icons/menu.svg b/src/icons/menu.svg new file mode 100644 index 0000000..64844e7 --- /dev/null +++ b/src/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/palette.svg b/src/icons/palette.svg new file mode 100644 index 0000000..ebf6936 --- /dev/null +++ b/src/icons/palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/refresh.svg b/src/icons/refresh.svg new file mode 100644 index 0000000..ebe3f16 --- /dev/null +++ b/src/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/settings.svg b/src/icons/settings.svg new file mode 100644 index 0000000..731a5a7 --- /dev/null +++ b/src/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/kristall.pro b/src/kristall.pro new file mode 100644 index 0000000..6aa5f60 --- /dev/null +++ b/src/kristall.pro @@ -0,0 +1,53 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets network + +CONFIG += c++17 + +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + browsertab.cpp \ + documentoutlinemodel.cpp \ + favouritecollection.cpp \ + geminiclient.cpp \ + geminirenderer.cpp \ + main.cpp \ + mainwindow.cpp \ + settingsdialog.cpp \ + tabbrowsinghistory.cpp + +HEADERS += \ + browsertab.hpp \ + documentoutlinemodel.hpp \ + favouritecollection.hpp \ + geminiclient.hpp \ + geminirenderer.hpp \ + mainwindow.hpp \ + settingsdialog.hpp \ + tabbrowsinghistory.hpp + +FORMS += \ + browsertab.ui \ + mainwindow.ui \ + settingsdialog.ui + +TRANSLATIONS += \ + kristall_en_US.ts + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +RESOURCES += \ + icons.qrc diff --git a/src/kristall_en_US.ts b/src/kristall_en_US.ts new file mode 100644 index 0000000..ef2593b --- /dev/null +++ b/src/kristall_en_US.ts @@ -0,0 +1,3 @@ + + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..31cc7da --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,18 @@ +#include "mainwindow.hpp" + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + + + + w.addNewTab(true, QUrl("gemini://gemini.circumlunar.space/")); + + w.show(); + return a.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..19dd922 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,259 @@ +#include "mainwindow.hpp" +#include "ui_mainwindow.h" +#include "browsertab.hpp" +#include "settingsdialog.hpp" + +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + settings("xqTechnologies", "Kristall"), + ui(new Ui::MainWindow), + url_status(new QLabel()) +{ + ui->setupUi(this); + + this->statusBar()->addWidget(this->url_status); + + this->favourites.load(settings); + this->current_style.load(settings); + + ui->favourites_view->setModel(&favourites); + + // this->ui->history_window->setVisible(false); + this->ui->clientcert_window->setVisible(false); + this->ui->bookmarks_window->setVisible(true); + + for(QDockWidget * dock : findChildren()) + { + QAction * act = this->ui->menuView ->addAction(dock->windowTitle()); + act->setCheckable(true); + act->setChecked(dock->isVisible()); + act->setData(QVariant::fromValue(dock)); + connect(act, QOverload::of(&QAction::triggered), dock, &QDockWidget::setVisible); + } + + connect(this->ui->menuView, &QMenu::aboutToShow, [this]() { + for(QAction * act : this->ui->menuView->actions()) + { + auto * dock = qvariant_cast(act->data()); + act->setChecked(dock->isVisible()); + } + }); + + + { + settings.beginGroup("Window State"); + if(settings.contains("geometry")) { + restoreGeometry(settings.value("geometry").toByteArray()); + } + if(settings.contains("state")) { + restoreState(settings.value("state").toByteArray()); + } + settings.endGroup(); + } +} + +MainWindow::~MainWindow() +{ + + this->saveSettings(); + delete ui; +} + +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) { + this->ui->browser_tabs->setCurrentIndex(index); + } + + return tab; +} + +BrowserTab * MainWindow::addNewTab(bool focus_new, QUrl const & url) +{ + auto tab = addEmptyTab(focus_new); + tab->navigateTo(url, BrowserTab::PushImmediate); + return tab; +} + +void MainWindow::setUrlPreview(const QUrl &url) +{ + if(url.isValid()) { + auto str = url.toString(); + if(str.length() > 300) { + str = str.mid(0, 300) + "..."; + } + this->url_status->setText(str); + } + else { + this->url_status->setText(""); + } +} + +void MainWindow::saveSettings() +{ + this->favourites.save(settings); + this->current_style.save(settings); + + { + settings.beginGroup("Window State"); + + settings.setValue("geometry", saveGeometry()); + settings.setValue("state", saveState()); + + settings.endGroup(); + } +} + +void MainWindow::on_browser_tabs_currentChanged(int index) +{ + if(index >= 0) { + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->widget(index)); + + if(tab != nullptr) { + this->ui->outline_view->setModel(&tab->outline); + this->ui->outline_view->expandAll(); + + this->ui->history_view->setModel(&tab->history); + } else { + this->ui->outline_view->setModel(nullptr); + this->ui->history_view->setModel(nullptr); + } + } else { + this->ui->outline_view->setModel(nullptr); + this->ui->history_view->setModel(nullptr); + } +} + +void MainWindow::on_favourites_view_doubleClicked(const QModelIndex &index) +{ + if(auto url = this->favourites.get(index); url.isValid()) { + this->addNewTab(true, url); + } +} + +void MainWindow::on_browser_tabs_tabCloseRequested(int index) +{ + delete this->ui->browser_tabs->widget(index); +} + +void MainWindow::on_history_view_doubleClicked(const QModelIndex &index) +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navigateBack(index); + } +} + +void MainWindow::on_tab_titleChanged(const QString &title) +{ + auto * tab = qobject_cast(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(sender()); + if(tab != nullptr) { + int index = this->ui->browser_tabs->indexOf(tab); + assert(index >= 0); + this->ui->browser_tabs->setTabToolTip(index, url.toString()); + } +} + +void MainWindow::on_outline_view_clicked(const QModelIndex &index) +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + + auto anchor = tab->outline.getAnchor(index); + if(not anchor.isEmpty()) { + tab->scrollToAnchor(anchor); + } + } +} + +void MainWindow::on_actionSettings_triggered() +{ + SettingsDialog dialog; + + dialog.setGeminiStyle(this->current_style); + + if(dialog.exec() == QDialog::Accepted) { + this->current_style = dialog.geminiStyle(); + this->saveSettings(); + } +} + +void MainWindow::on_actionNew_Tab_triggered() +{ + this->addEmptyTab(true); +} + +void MainWindow::on_actionQuit_triggered() +{ + QApplication::quit(); +} + +void MainWindow::on_actionAbout_triggered() +{ + QMessageBox::about(this, + "Kristall", +R"about(Kristall, an OpenSource Gemini browser. +Made by Felix "xq" Queißner + +This is free software. You can get the source code at +https://github.com/MasterQ32/Kristall)about" + ); +} + +void MainWindow::on_actionClose_Tab_triggered() +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + delete tab; + } +} + +void MainWindow::on_actionForward_triggered() +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navOneForward(); + } +} + +void MainWindow::on_actionBackward_triggered() +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navOneBackback(); + } +} + +void MainWindow::on_actionRefresh_triggered() +{ + BrowserTab * tab = qobject_cast(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->reloadPage(); + } +} + +void MainWindow::on_actionAbout_Qt_triggered() +{ + QMessageBox::aboutQt(this, "Kristall"); +} diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp new file mode 100644 index 0000000..41b6e2c --- /dev/null +++ b/src/mainwindow.hpp @@ -0,0 +1,78 @@ +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP + +#include +#include +#include + + +#include "favouritecollection.hpp" +#include "geminirenderer.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class BrowserTab; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + BrowserTab * addEmptyTab(bool focus_new); + BrowserTab * addNewTab(bool focus_new, QUrl const & url); + + void setUrlPreview(QUrl const & url); + + void saveSettings(); + +public: + FavouriteCollection favourites; + +private slots: + void on_browser_tabs_currentChanged(int index); + + void on_favourites_view_doubleClicked(const QModelIndex &index); + + void on_browser_tabs_tabCloseRequested(int index); + + void on_history_view_doubleClicked(const QModelIndex &index); + + void on_tab_titleChanged(QString const & title); + + void on_tab_locationChanged(QUrl const & url); + + void on_outline_view_clicked(const QModelIndex &index); + + void on_actionSettings_triggered(); + + void on_actionNew_Tab_triggered(); + + void on_actionQuit_triggered(); + + void on_actionAbout_triggered(); + + void on_actionClose_Tab_triggered(); + + void on_actionForward_triggered(); + + void on_actionBackward_triggered(); + + void on_actionRefresh_triggered(); + + void on_actionAbout_Qt_triggered(); + +public: + QSettings settings; + GeminiStyle current_style; + +private: + Ui::MainWindow *ui; + + QLabel * url_status; +}; +#endif // MAINWINDOW_HPP diff --git a/src/mainwindow.ui b/src/mainwindow.ui new file mode 100644 index 0000000..08ba787 --- /dev/null +++ b/src/mainwindow.ui @@ -0,0 +1,309 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Kristall Browser + + + + :/icons/kristall.svg:/icons/kristall.svg + + + QTabWidget::Rounded + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + -1 + + + true + + + true + + + true + + + true + + + + + + + + + Document Outline + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + false + + + + + + + + + + :/icons/heart.svg:/icons/heart.svg + + + Bookmarks + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + History + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + Client Certificates + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + + + + 0 + 0 + 800 + 20 + + + + + File + + + + + + + + + + + Help + + + + + + + View + + + + + Navigation + + + + + + + + + + + + + + About... + + + + + Quit + + + + + New Tab + + + Ctrl+T + + + + + Close Tab + + + Ctrl+W + + + + + + :/icons/settings.svg:/icons/settings.svg + + + Settings + + + + + + :/icons/arrow-left.svg:/icons/arrow-left.svg + + + Backward + + + Alt+Left + + + + + + :/icons/arrow-right.svg:/icons/arrow-right.svg + + + Forward + + + Alt+Right + + + + + + :/icons/refresh.svg:/icons/refresh.svg + + + Refresh + + + F5 + + + + + About Qt... + + + + + + + + diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp new file mode 100644 index 0000000..2078e7c --- /dev/null +++ b/src/settingsdialog.cpp @@ -0,0 +1,255 @@ +#include "settingsdialog.hpp" +#include "ui_settingsdialog.h" +#include +#include +#include + +SettingsDialog::SettingsDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::SettingsDialog), + current_style() +{ + ui->setupUi(this); + + static_assert(GeminiStyle::Fixed == 0); + static_assert(GeminiStyle::AutoDarkTheme == 1); + static_assert(GeminiStyle::AutoLightTheme == 2); + + this->ui->auto_theme->clear(); + this->ui->auto_theme->addItem("Disabled", QVariant::fromValue(GeminiStyle::Fixed)); + this->ui->auto_theme->addItem("Dark Theme", QVariant::fromValue(GeminiStyle::AutoDarkTheme)); + this->ui->auto_theme->addItem("Light Theme", QVariant::fromValue(GeminiStyle::AutoLightTheme)); + + setGeminiStyle(GeminiStyle { }); +} + +SettingsDialog::~SettingsDialog() +{ + delete ui; +} + +static QString formatFont(QFont const & font) +{ + QString style; + if(font.italic() and font.bold()) + style = "bold, italic"; + else if(font.italic()) + style = "italic"; + else if(font.bold()) + style = "bold"; + else + style = "regular"; + + return QString("%1 (%2pt, %3)") + .arg(font.family()) + .arg(font.pointSizeF()) + .arg(style); +} + +void SettingsDialog::setGeminiStyle(const GeminiStyle &style) +{ + static const QString COLOR_STYLE("border: 1px solid black; padding: 4px; background-color : %1; color : %2;"); + + this->current_style = style; + + this->ui->auto_theme->setCurrentIndex(this->current_style.theme); + + this->ui->page_margin->setValue(this->current_style.margin); + + auto setFontAndColor = [this](QLabel * label, QFont font, QColor color) + { + label->setText(formatFont(font)); + label->setStyleSheet(COLOR_STYLE + .arg(this->current_style.background_color.name()) + .arg(color.name())); + }; + + ui->bg_preview->setStyleSheet(COLOR_STYLE + .arg(this->current_style.background_color.name()) + .arg("#FF00FF")); + + ui->link_local_preview->setStyleSheet(COLOR_STYLE + .arg(this->current_style.background_color.name()) + .arg(this->current_style.internal_link_color.name())); + + ui->link_foreign_preview->setStyleSheet(COLOR_STYLE + .arg(this->current_style.background_color.name()) + .arg(this->current_style.external_link_color.name())); + + ui->link_cross_preview->setStyleSheet(COLOR_STYLE + .arg(this->current_style.background_color.name()) + .arg(this->current_style.cross_scheme_link_color.name())); + + setFontAndColor(this->ui->std_preview, this->current_style.standard_font, this->current_style.standard_color); + setFontAndColor(this->ui->pre_preview, this->current_style.preformatted_font, this->current_style.preformatted_color); + setFontAndColor(this->ui->h1_preview, this->current_style.h1_font, this->current_style.h1_color); + setFontAndColor(this->ui->h2_preview, this->current_style.h2_font, this->current_style.h2_color); + setFontAndColor(this->ui->h3_preview, this->current_style.h3_font, this->current_style.h3_color); + + this->reloadStylePreview(); +} + +void SettingsDialog::reloadStylePreview() +{ + auto const document = R"gemini(# H1 Header +## H2 Header +### H3 Header +Plain text document here. +* List A +* List B +=> rela-link Same-Site Link +=> //foreign.host/ Foreign Site Link +=> https://foreign.host/ Cross-Protocol Link +``` + ▄▄▄ ██▀███ ▄▄▄█████▓ + ▒████▄ ▓██ ▒ ██▒▓ ██▒ ▓▒ + ▒██ ▀█▄ ▓██ ░▄█ ▒▒ ▓██░ ▒░ + ░██▄▄▄▄██ ▒██▀▀█▄ ░ ▓██▓ ░ + ▓█ ▓██▒░██▓ ▒██▒ ▒██▒ ░ + ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒ ░░ + ▒ ▒▒ ░ ░▒ ░ ▒░ ░ + ░ ▒ ░░ ░ ░ + ░ ░ ░ +)gemini"; + + QString host = this->ui->preview_url->text(); + if(host.length() == 0) + host = "preview"; + + DocumentOutlineModel outline; + auto doc = GeminiRenderer { current_style }.render( + document, + QUrl(QString("about://%1/foobar").arg(host)), + outline + ); + + ui->style_preview->setStyleSheet(QString("QTextBrowser { background-color: %1; }") + .arg(doc->background_color.name())); + ui->style_preview->setDocument(doc.get()); + preview_document = std::move(doc); +} + +void SettingsDialog::updateFont(QFont & input) +{ + QFontDialog dialog { this }; + + dialog.setCurrentFont(input); + + if(dialog.exec() == QDialog::Accepted) { + input = dialog.currentFont(); + setGeminiStyle(current_style); + } +} + +void SettingsDialog::on_std_change_font_clicked() +{ + updateFont(current_style.standard_font); +} + +void SettingsDialog::on_pre_change_font_clicked() +{ + updateFont(current_style.preformatted_font); +} + +void SettingsDialog::on_h1_change_font_clicked() +{ + updateFont(current_style.h1_font); +} + +void SettingsDialog::on_h2_change_font_clicked() +{ + updateFont(current_style.h2_font); +} + +void SettingsDialog::on_h3_change_font_clicked() +{ + updateFont(current_style.h3_font); +} + +void SettingsDialog::updateColor(QColor &input) +{ + QColorDialog dialog { this }; + + dialog.setCurrentColor(input); + + if(dialog.exec() == QDialog::Accepted) { + input = dialog.currentColor(); + setGeminiStyle(current_style); + } +} + +void SettingsDialog::on_std_change_color_clicked() +{ + updateColor(current_style.standard_color); +} + +void SettingsDialog::on_pre_change_color_clicked() +{ + updateColor(current_style.preformatted_color); +} + +void SettingsDialog::on_h1_change_color_clicked() +{ + updateColor(current_style.h1_color); +} + +void SettingsDialog::on_h2_change_color_clicked() +{ + updateColor(current_style.h2_color); +} + +void SettingsDialog::on_h3_change_color_clicked() +{ + updateColor(current_style.h3_color); +} + +void SettingsDialog::on_bg_change_color_clicked() +{ + updateColor(current_style.background_color); +} + +void SettingsDialog::on_link_local_change_color_clicked() +{ + updateColor(current_style.internal_link_color); +} + +void SettingsDialog::on_link_foreign_change_color_clicked() +{ + updateColor(current_style.external_link_color); +} + +void SettingsDialog::on_link_cross_change_color_clicked() +{ + updateColor(current_style.cross_scheme_link_color); +} + +void SettingsDialog::on_link_local_prefix_textChanged(const QString &text) +{ + current_style.internal_link_prefix = text; + reloadStylePreview(); +} + +void SettingsDialog::on_link_foreign_prefix_textChanged(const QString &text) +{ + current_style.external_link_prefix = text; + reloadStylePreview(); +} + +void SettingsDialog::on_auto_theme_currentIndexChanged(int index) +{ + if(index >= 0) { + current_style.theme = GeminiStyle::Theme(index); + reloadStylePreview(); + } +} + +void SettingsDialog::on_preview_url_textChanged(const QString &arg1) +{ + this->reloadStylePreview(); +} + +void SettingsDialog::on_page_margin_valueChanged(double value) +{ + this->current_style.margin = value; + this->reloadStylePreview(); +} diff --git a/src/settingsdialog.hpp b/src/settingsdialog.hpp new file mode 100644 index 0000000..5f56961 --- /dev/null +++ b/src/settingsdialog.hpp @@ -0,0 +1,79 @@ +#ifndef SETTINGSDIALOG_HPP +#define SETTINGSDIALOG_HPP + +#include + +#include "geminirenderer.hpp" + +namespace Ui { +class SettingsDialog; +} + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget *parent = nullptr); + ~SettingsDialog(); + + void setGeminiStyle(GeminiStyle const & style); + + GeminiStyle geminiStyle() const { + return current_style; + } + +private slots: + void on_std_change_font_clicked(); + + void on_pre_change_font_clicked(); + + void on_h1_change_font_clicked(); + + void on_h2_change_font_clicked(); + + void on_h3_change_font_clicked(); + + void on_std_change_color_clicked(); + + void on_pre_change_color_clicked(); + + void on_h1_change_color_clicked(); + + void on_h2_change_color_clicked(); + + void on_h3_change_color_clicked(); + + void on_bg_change_color_clicked(); + + void on_link_local_change_color_clicked(); + + void on_link_foreign_change_color_clicked(); + + void on_link_cross_change_color_clicked(); + + void on_link_local_prefix_textChanged(const QString &arg1); + + void on_link_foreign_prefix_textChanged(const QString &arg1); + + void on_auto_theme_currentIndexChanged(int index); + + void on_preview_url_textChanged(const QString &arg1); + + void on_page_margin_valueChanged(double arg1); + +private: + void reloadStylePreview(); + + void updateFont(QFont & input); + + void updateColor(QColor & input); + +private: + Ui::SettingsDialog *ui; + + GeminiStyle current_style; + std::unique_ptr preview_document; +}; + +#endif // SETTINGSDIALOG_HPP diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui new file mode 100644 index 0000000..aee3959 --- /dev/null +++ b/src/settingsdialog.ui @@ -0,0 +1,517 @@ + + + SettingsDialog + + + + 0 + 0 + 800 + 520 + + + + Settings + + + + + + 0 + + + + Style + + + + + + 5 + + + + + + + + + + Qt::PlainText + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + Background Color + + + + + + + Standard Font + + + + + + + + + QFrame::NoFrame + + + This text will be displayed for normal text. + + + + + + + + + + + :/icons/format-font.svg:/icons/format-font.svg + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + Preformatted Font + + + + + + + + + QFrame::NoFrame + + + This text will be displayed for preformatted text. + + + + + + + + + + + :/icons/format-font.svg:/icons/format-font.svg + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + H1 Font + + + + + + + + + QFrame::NoFrame + + + This text will be displayed for a level 1 heading. + + + + + + + + + + + :/icons/format-font.svg:/icons/format-font.svg + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + H2 Font + + + + + + + + + QFrame::NoFrame + + + This text will be displayed for a level 2 heading. + + + + + + + + + + + :/icons/format-font.svg:/icons/format-font.svg + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + H3 Font + + + + + + + + + border: 1px solid black; + + + QFrame::NoFrame + + + This text will be displayed for a level 3 heading. + + + Qt::PlainText + + + + + + + + + + + :/icons/format-font.svg:/icons/format-font.svg + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + Local Link Color + + + + + + + Foreign Link Color + + + + + + + Cross-Scheme-Color + + + + + + + Local Link Prefix + + + + + + + Extern Link Prefix + + + + + + + + + + + + + + + + + + + + + + + This is a local reference + + + Qt::PlainText + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + + + This is a foreign reference + + + Qt::PlainText + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + + + This reference is cross-scheme + + + Qt::PlainText + + + + + + + + + + + :/icons/palette.svg:/icons/palette.svg + + + + + + + + + Auto-Theme Generation + + + + + + + + + + Page Margin + + + + + + + px + + + 0 + + + 350.000000000000000 + + + + + + + + + + + false + + + + + + + host.name + + + + + + + + + + Generic + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 325 + 470 + + + 286 + 274 + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 257 + 470 + + + 157 + 274 + + + + + diff --git a/src/tabbrowsinghistory.cpp b/src/tabbrowsinghistory.cpp new file mode 100644 index 0000000..435bca7 --- /dev/null +++ b/src/tabbrowsinghistory.cpp @@ -0,0 +1,79 @@ +#include "tabbrowsinghistory.hpp" + +TabBrowsingHistory::TabBrowsingHistory() +{ + +} + +bool TabBrowsingHistory::canGoBack() const +{ + return this->history.size() > 0; +} + +bool TabBrowsingHistory::canGoForward() const +{ + return false; +} + +QModelIndex TabBrowsingHistory::pushUrl(QModelIndex const & position, const QUrl &url) +{ + this->beginInsertRows(QModelIndex{}, this->history.length(),this->history.length() + 1); + + if(position.isValid()) { + this->history.resize(position.row() + 1); + } + + this->history.push_back(url); + + this->endInsertRows(); + + return this->createIndex(this->history.size() - 1, 0); +} + +QUrl TabBrowsingHistory::get(const QModelIndex &index) const +{ + if(not index.isValid()) + return QUrl { }; + + if(index.row() >= history.size()) + return QUrl { }; + else + return history.at(index.row()); +} + +QModelIndex TabBrowsingHistory::oneForward(QModelIndex index) const +{ + if(not index.isValid()) + return QModelIndex{}; + if(index.row() >= history.size() - 1) + return QModelIndex{}; + return createIndex(index.row() + 1, index.column()); +} + +QModelIndex TabBrowsingHistory::oneBackward(QModelIndex index) const +{ + if(not index.isValid()) + return QModelIndex{}; + if(index.row() == 0) + return QModelIndex{}; + return createIndex(index.row() - 1, index.column()); +} + +int TabBrowsingHistory::rowCount(const QModelIndex &parent) const +{ + return history.size(); +} + +bool TabBrowsingHistory::setData(const QModelIndex &index, const QVariant &value, int role) +{ + return false; +} + +QVariant TabBrowsingHistory::data(const QModelIndex &index, int role) const +{ + if(role != Qt::DisplayRole) { + return QVariant{}; + } + return history.at(index.row()).toString(); +} + diff --git a/src/tabbrowsinghistory.hpp b/src/tabbrowsinghistory.hpp new file mode 100644 index 0000000..4305218 --- /dev/null +++ b/src/tabbrowsinghistory.hpp @@ -0,0 +1,39 @@ +#ifndef TABBROWSINGHISTORY_HPP +#define TABBROWSINGHISTORY_HPP + +#include +#include +#include + +class TabBrowsingHistory : + public QAbstractListModel +{ + Q_OBJECT +public: + TabBrowsingHistory(); + + bool canGoBack() const; + + bool canGoForward() const; + + QModelIndex pushUrl(QModelIndex const & position, QUrl const & url); + + QUrl get(QModelIndex const & index) const; + + QModelIndex oneForward(QModelIndex index) const; + + QModelIndex oneBackward(QModelIndex index) const; + +public: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + +private: + QVector history; +}; + +#endif // TABBROWSINGHISTORY_HPP diff --git a/tabbrowsinghistory.cpp b/tabbrowsinghistory.cpp deleted file mode 100644 index 435bca7..0000000 --- a/tabbrowsinghistory.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "tabbrowsinghistory.hpp" - -TabBrowsingHistory::TabBrowsingHistory() -{ - -} - -bool TabBrowsingHistory::canGoBack() const -{ - return this->history.size() > 0; -} - -bool TabBrowsingHistory::canGoForward() const -{ - return false; -} - -QModelIndex TabBrowsingHistory::pushUrl(QModelIndex const & position, const QUrl &url) -{ - this->beginInsertRows(QModelIndex{}, this->history.length(),this->history.length() + 1); - - if(position.isValid()) { - this->history.resize(position.row() + 1); - } - - this->history.push_back(url); - - this->endInsertRows(); - - return this->createIndex(this->history.size() - 1, 0); -} - -QUrl TabBrowsingHistory::get(const QModelIndex &index) const -{ - if(not index.isValid()) - return QUrl { }; - - if(index.row() >= history.size()) - return QUrl { }; - else - return history.at(index.row()); -} - -QModelIndex TabBrowsingHistory::oneForward(QModelIndex index) const -{ - if(not index.isValid()) - return QModelIndex{}; - if(index.row() >= history.size() - 1) - return QModelIndex{}; - return createIndex(index.row() + 1, index.column()); -} - -QModelIndex TabBrowsingHistory::oneBackward(QModelIndex index) const -{ - if(not index.isValid()) - return QModelIndex{}; - if(index.row() == 0) - return QModelIndex{}; - return createIndex(index.row() - 1, index.column()); -} - -int TabBrowsingHistory::rowCount(const QModelIndex &parent) const -{ - return history.size(); -} - -bool TabBrowsingHistory::setData(const QModelIndex &index, const QVariant &value, int role) -{ - return false; -} - -QVariant TabBrowsingHistory::data(const QModelIndex &index, int role) const -{ - if(role != Qt::DisplayRole) { - return QVariant{}; - } - return history.at(index.row()).toString(); -} - diff --git a/tabbrowsinghistory.hpp b/tabbrowsinghistory.hpp deleted file mode 100644 index 4305218..0000000 --- a/tabbrowsinghistory.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef TABBROWSINGHISTORY_HPP -#define TABBROWSINGHISTORY_HPP - -#include -#include -#include - -class TabBrowsingHistory : - public QAbstractListModel -{ - Q_OBJECT -public: - TabBrowsingHistory(); - - bool canGoBack() const; - - bool canGoForward() const; - - QModelIndex pushUrl(QModelIndex const & position, QUrl const & url); - - QUrl get(QModelIndex const & index) const; - - QModelIndex oneForward(QModelIndex index) const; - - QModelIndex oneBackward(QModelIndex index) const; - -public: - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - -private: - QVector history; -}; - -#endif // TABBROWSINGHISTORY_HPP -- cgit v1.2.3