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. --- 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 ++++ 34 files changed, 3695 insertions(+) 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 (limited to 'src') 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 -- cgit v1.2.3