diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-06 23:14:21 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-06 23:14:21 +0200 |
| commit | 3aed883402dc8da829fc304434c5efd0570cbb97 (patch) | |
| tree | 48c46ab087a950d80f78819ceb609e93d246b040 /src | |
| parent | 44e85dce678e7e36f436a6d0a25c212c9a2d3657 (diff) | |
| download | kristall-3aed883402dc8da829fc304434c5efd0570cbb97.tar.gz | |
Moves source code into subdirectory.
Diffstat (limited to 'src')
34 files changed, 3695 insertions, 0 deletions
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 <QTabWidget> +#include <QMenu> +#include <QMessageBox> +#include <QInputDialog> +#include <QDockWidget> +#include <QImage> +#include <QPixmap> + +#include <QGraphicsPixmapItem> +#include <QGraphicsTextItem> + + +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<QTextDocument> 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<QTextDocument>(); + document->setHtml(QString::fromUtf8(data)); + } +#if defined(QT_FEATURE_textmarkdownreader) + else if(mime.startsWith("text/markdown")) { + document = std::make_unique<QTextDocument>(); + document->setMarkdown(QString::fromUtf8(data)); + } +#endif + else if(mime.startsWith("text/")) { + QFont monospace; + monospace.setFamily("monospace"); + + document = std::make_unique<QTextDocument>(); + document->setDefaultFont(monospace); + document->setPlainText(QString::fromUtf8(data)); + } + else if(mime.startsWith("image/")) { + + QImage img; + if(img.loadFromData(data, nullptr)) + { + 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 <QWidget> +#include <QUrl> +#include <QGraphicsScene> +#include <QTextDocument> + +#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<QTextDocument> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BrowserTab</class> + <widget class="QWidget" name="BrowserTab"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>692</width> + <height>404</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item> + <widget class="QToolButton" name="back_button"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/arrow-left.svg</normaloff>:/icons/arrow-left.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="forward_button"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>...</string> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/arrow-right.svg</normaloff>:/icons/arrow-right.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="stop_button"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/close.svg</normaloff>:/icons/close.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="refresh_button"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/refresh.svg</normaloff>:/icons/refresh.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="url_bar"> + <property name="placeholderText"> + <string>gemini://</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="fav_button"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/heart-outline.svg</normaloff> + <normalon>:/icons/heart.svg</normalon>:/icons/heart-outline.svg</iconset> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <item> + <widget class="QTextBrowser" name="text_browser"> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="autoFormatting"> + <set>QTextEdit::AutoNone</set> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="html"> + <string><!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></string> + </property> + <property name="tabStopWidth"> + <number>40</number> + </property> + <property name="openLinks"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QGraphicsView" name="graphics_browser"> + <property name="dragMode"> + <enum>QGraphicsView::ScrollHandDrag</enum> + </property> + <property name="transformationAnchor"> + <enum>QGraphicsView::AnchorUnderMouse</enum> + </property> + <property name="resizeAnchor"> + <enum>QGraphicsView::AnchorUnderMouse</enum> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="icons.qrc"/> + </resources> + <connections/> +</ui> 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 <QModelIndex> + +DocumentOutlineModel::DocumentOutlineModel() : + QAbstractItemModel(), + root() +{ + +} + +void DocumentOutlineModel::clear() +{ + beginBuild(); + endBuild(); + +} + +void DocumentOutlineModel::beginBuild() +{ + beginResetModel(); + root = Node { + nullptr, + "<ROOT>", "", + 0, 0, + QList<Node> { }, + }; +} + +void DocumentOutlineModel::appendH1(const QString &title, QString const & anchor) +{ + root.children.append(Node { + &root, + title, anchor, + 1, 0, + QList<Node> { }, + }); +} + +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<Node> { }, + }); +} + +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<Node> { }, + }); +} + +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<Node const *>(index.internalPointer()); + + return childItem->title; +} + +QString DocumentOutlineModel::getAnchor(const QModelIndex &index) const +{ + if(not index.isValid()) + return ""; + + Node const *childItem = static_cast<Node const *>(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<Node*>(parent.internalPointer()); + + Node const * childItem = &parentItem->children[row]; + if (childItem) + return createIndex(row, column, reinterpret_cast<quintptr>(childItem)); + return QModelIndex(); + +} + +QModelIndex DocumentOutlineModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) + return QModelIndex(); + + Node const *childItem = static_cast<Node const *>(child.internalPointer()); + Node const * parent = childItem->parent; + + if (parent == &root) + return QModelIndex(); + + return createIndex( + parent->index, + 0, + reinterpret_cast<quintptr>(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<Node const *>(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<Node const*>(index.internalPointer()); + + return item->title; +} + +DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel1() +{ + if(root.children.size() == 0) { + root.children.append(Node { + &root, + "<missing layer>", "", + 1, 0, + QList<Node> { }, + }); + } + return root.children.last(); +} + +DocumentOutlineModel::Node & DocumentOutlineModel::ensureLevel2() +{ + auto & parent = ensureLevel1(); + + if(parent.children.size() == 0) { + root.children.append(Node { + &parent, + "<missing layer>", "", + 2, 0, + QList<Node> { }, + }); + } + + 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 <QAbstractItemModel> +#include <QList> + +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<Node> 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 <QFile> + +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 <QObject> +#include <QAbstractListModel> +#include <QUrl> +#include <QSettings> + + +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<QUrl> 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 <QDebug> + +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<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); + connect(&socket, QOverload<QAbstractSocket::SocketError>::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 = "<invalid>"; + + 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 " <META> <CR> <LF> + 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 <CR> <LF>"); + 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<QSslError> &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 <QObject> +#include <QMimeType> +#include <QSslSocket> +#include <QUrl> + +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<QSslError> &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 <QTextList> +#include <QCryptographicHash> +#include <QTextBlock> +#include <QDebug> +#include <cmath> + +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<uint8_t, 16> 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<GeminiDocument> 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<GeminiDocument> result = std::make_unique<GeminiDocument>(); + 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<QByteArray> 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 <QTextDocument> +#include <QColor> +#include <QSettings> + +#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<GeminiDocument> 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 @@ +<RCC> + <qresource prefix="/"> + <file>icons/arrow-left.svg</file> + <file>icons/arrow-right.svg</file> + <file>icons/heart-outline.svg</file> + <file>icons/heart.svg</file> + <file>icons/menu.svg</file> + <file>icons/refresh.svg</file> + <file>icons/close.svg</file> + <file>icons/format-font.svg</file> + <file>icons/palette.svg</file> + <file>icons/kristall.svg</file> + <file>icons/settings.svg</file> + </qresource> +</RCC> 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg>
\ No newline at end of file diff --git a/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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17,8H20V20H21V21H17V20H18V17H14L12.5,20H14V21H10V20H11L17,8M18,9L14.5,16H18V9M5,3H10C11.11,3 12,3.89 12,5V16H9V11H6V16H3V5C3,3.89 3.89,3 5,3M6,5V9H9V5H6Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + height="24" + width="24" + version="1.1"> + <path + d="M 3.1369986,7.8561215 6.2997316,2.8956626 17.476275,2.7919947 20.786815,7.9230405 11.777946,20.606669 Z" + style="fill:#7595ff" /> + <path + d="m 16,9 h 3 l -5,7 M 10,9 h 4 l -2,8 M 5,9 H 8.0000002 L 10,16 M 15,4 h 2 l 2,3 H 16 M 11,4 h 2 l 1,3 H 10 M 7,4 H 9 L 8,7 H 5 M 6,2 2,8 12,22 22,8 18,2 Z" /> +</svg> 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>
\ 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="kristall_en_US"></TS> 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 <QApplication> +#include <QUrl> +#include <QSettings> + +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 <QMessageBox> +#include <memory> +#include <QShortcut> +#include <QKeySequence> + +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<QDockWidget *>()) + { + QAction * act = this->ui->menuView ->addAction(dock->windowTitle()); + act->setCheckable(true); + act->setChecked(dock->isVisible()); + act->setData(QVariant::fromValue(dock)); + connect(act, QOverload<bool>::of(&QAction::triggered), dock, &QDockWidget::setVisible); + } + + connect(this->ui->menuView, &QMenu::aboutToShow, [this]() { + for(QAction * act : this->ui->menuView->actions()) + { + auto * dock = qvariant_cast<QDockWidget*>(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<BrowserTab*>(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<BrowserTab*>(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navigateBack(index); + } +} + +void MainWindow::on_tab_titleChanged(const QString &title) +{ + auto * tab = qobject_cast<BrowserTab*>(sender()); + if(tab != nullptr) { + int index = this->ui->browser_tabs->indexOf(tab); + assert(index >= 0); + this->ui->browser_tabs->setTabText(index, title); + } +} + +void MainWindow::on_tab_locationChanged(const QUrl &url) +{ + auto * tab = qobject_cast<BrowserTab*>(sender()); + if(tab != nullptr) { + int index = this->ui->browser_tabs->indexOf(tab); + assert(index >= 0); + this->ui->browser_tabs->setTabToolTip(index, url.toString()); + } +} + +void MainWindow::on_outline_view_clicked(const QModelIndex &index) +{ + BrowserTab * tab = qobject_cast<BrowserTab*>(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<BrowserTab*>(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + delete tab; + } +} + +void MainWindow::on_actionForward_triggered() +{ + BrowserTab * tab = qobject_cast<BrowserTab*>(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navOneForward(); + } +} + +void MainWindow::on_actionBackward_triggered() +{ + BrowserTab * tab = qobject_cast<BrowserTab*>(this->ui->browser_tabs->currentWidget()); + if(tab != nullptr) { + tab->navOneBackback(); + } +} + +void MainWindow::on_actionRefresh_triggered() +{ + BrowserTab * tab = qobject_cast<BrowserTab*>(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 <QMainWindow> +#include <QLabel> +#include <QSettings> + + +#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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>Kristall Browser</string> + </property> + <property name="windowIcon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/kristall.svg</normaloff>:/icons/kristall.svg</iconset> + </property> + <property name="tabShape"> + <enum>QTabWidget::Rounded</enum> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTabWidget" name="browser_tabs"> + <property name="currentIndex"> + <number>-1</number> + </property> + <property name="documentMode"> + <bool>true</bool> + </property> + <property name="tabsClosable"> + <bool>true</bool> + </property> + <property name="movable"> + <bool>true</bool> + </property> + <property name="tabBarAutoHide"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QStatusBar" name="statusBar"/> + <widget class="QDockWidget" name="outline_window"> + <property name="windowTitle"> + <string>Document Outline</string> + </property> + <attribute name="dockWidgetArea"> + <number>1</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeView" name="outline_view"> + <property name="autoExpandDelay"> + <number>0</number> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </widget> + <widget class="QDockWidget" name="bookmarks_window"> + <property name="windowIcon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/heart.svg</normaloff>:/icons/heart.svg</iconset> + </property> + <property name="windowTitle"> + <string>Bookmarks</string> + </property> + <attribute name="dockWidgetArea"> + <number>2</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents_2"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QListView" name="favourites_view"/> + </item> + </layout> + </widget> + </widget> + <widget class="QDockWidget" name="history_window"> + <property name="windowTitle"> + <string>History</string> + </property> + <attribute name="dockWidgetArea"> + <number>2</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents_3"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QListView" name="history_view"/> + </item> + </layout> + </widget> + </widget> + <widget class="QDockWidget" name="clientcert_window"> + <property name="windowTitle"> + <string>Client Certificates</string> + </property> + <attribute name="dockWidgetArea"> + <number>2</number> + </attribute> + <widget class="QWidget" name="dockWidgetContents_4"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeView" name="clientcert_view"> + <property name="headerHidden"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <widget class="QMenuBar" name="menuBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>20</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionNew_Tab"/> + <addaction name="actionClose_Tab"/> + <addaction name="separator"/> + <addaction name="actionSettings"/> + <addaction name="separator"/> + <addaction name="actionQuit"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>Help</string> + </property> + <addaction name="actionAbout"/> + <addaction name="actionAbout_Qt"/> + </widget> + <widget class="QMenu" name="menuView"> + <property name="title"> + <string>View</string> + </property> + </widget> + <widget class="QMenu" name="menuNavigation"> + <property name="title"> + <string>Navigation</string> + </property> + <addaction name="actionBackward"/> + <addaction name="actionForward"/> + <addaction name="separator"/> + <addaction name="actionRefresh"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menuNavigation"/> + <addaction name="menuView"/> + <addaction name="menuHelp"/> + </widget> + <action name="actionAbout"> + <property name="text"> + <string>About...</string> + </property> + </action> + <action name="actionQuit"> + <property name="text"> + <string>Quit</string> + </property> + </action> + <action name="actionNew_Tab"> + <property name="text"> + <string>New Tab</string> + </property> + <property name="shortcut"> + <string>Ctrl+T</string> + </property> + </action> + <action name="actionClose_Tab"> + <property name="text"> + <string>Close Tab</string> + </property> + <property name="shortcut"> + <string>Ctrl+W</string> + </property> + </action> + <action name="actionSettings"> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/settings.svg</normaloff>:/icons/settings.svg</iconset> + </property> + <property name="text"> + <string>Settings</string> + </property> + </action> + <action name="actionBackward"> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/arrow-left.svg</normaloff>:/icons/arrow-left.svg</iconset> + </property> + <property name="text"> + <string>Backward</string> + </property> + <property name="shortcut"> + <string>Alt+Left</string> + </property> + </action> + <action name="actionForward"> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/arrow-right.svg</normaloff>:/icons/arrow-right.svg</iconset> + </property> + <property name="text"> + <string>Forward</string> + </property> + <property name="shortcut"> + <string>Alt+Right</string> + </property> + </action> + <action name="actionRefresh"> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/refresh.svg</normaloff>:/icons/refresh.svg</iconset> + </property> + <property name="text"> + <string>Refresh</string> + </property> + <property name="shortcut"> + <string>F5</string> + </property> + </action> + <action name="actionAbout_Qt"> + <property name="text"> + <string>About Qt...</string> + </property> + </action> + </widget> + <resources> + <include location="icons.qrc"/> + </resources> + <connections/> +</ui> 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 <QFontDialog> +#include <QColorDialog> +#include <QStyle> + +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<int>(GeminiStyle::Fixed)); + this->ui->auto_theme->addItem("Dark Theme", QVariant::fromValue<int>(GeminiStyle::AutoDarkTheme)); + this->ui->auto_theme->addItem("Light Theme", QVariant::fromValue<int>(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 <QDialog> + +#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<QTextDocument> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SettingsDialog</class> + <widget class="QDialog" name="SettingsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>520</height> + </rect> + </property> + <property name="windowTitle"> + <string>Settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="style_tab"> + <attribute name="title"> + <string>Style</string> + </attribute> + <layout class="QHBoxLayout" name="horizontalLayout_23"> + <item> + <layout class="QFormLayout" name="formLayout_3"> + <property name="leftMargin"> + <number>5</number> + </property> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_13"> + <item> + <widget class="QLabel" name="bg_preview"> + <property name="text"> + <string/> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="bg_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Background Color</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Standard Font</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_14"> + <item> + <widget class="QLabel" name="std_preview"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="text"> + <string>This text will be displayed for normal text.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="std_change_font"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/format-font.svg</normaloff>:/icons/format-font.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="std_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Preformatted Font</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_15"> + <item> + <widget class="QLabel" name="pre_preview"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="text"> + <string>This text will be displayed for preformatted text.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="pre_change_font"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/format-font.svg</normaloff>:/icons/format-font.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="pre_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>H1 Font</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_16"> + <item> + <widget class="QLabel" name="h1_preview"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="text"> + <string>This text will be displayed for a level 1 heading.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h1_change_font"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/format-font.svg</normaloff>:/icons/format-font.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h1_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>H2 Font</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_18"> + <item> + <widget class="QLabel" name="h2_preview"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="text"> + <string>This text will be displayed for a level 2 heading.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h2_change_font"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/format-font.svg</normaloff>:/icons/format-font.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h2_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>H3 Font</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_19"> + <item> + <widget class="QLabel" name="h3_preview"> + <property name="styleSheet"> + <string notr="true">border: 1px solid black;</string> + </property> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="text"> + <string>This text will be displayed for a level 3 heading.</string> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h3_change_font"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/format-font.svg</normaloff>:/icons/format-font.svg</iconset> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="h3_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Local Link Color</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Foreign Link Color</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Cross-Scheme-Color</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Local Link Prefix</string> + </property> + </widget> + </item> + <item row="10" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Extern Link Prefix</string> + </property> + </widget> + </item> + <item row="10" column="1"> + <widget class="QLineEdit" name="link_foreign_prefix"> + <property name="text"> + <string>⇒ </string> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QLineEdit" name="link_local_prefix"> + <property name="text"> + <string>→ </string> + </property> + </widget> + </item> + <item row="6" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_20"> + <item> + <widget class="QLabel" name="link_local_preview"> + <property name="text"> + <string>This is a local reference</string> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="link_local_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="7" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_21"> + <item> + <widget class="QLabel" name="link_foreign_preview"> + <property name="text"> + <string>This is a foreign reference</string> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="link_foreign_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="8" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_22"> + <item> + <widget class="QLabel" name="link_cross_preview"> + <property name="text"> + <string>This reference is cross-scheme</string> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="link_cross_change_color"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </property> + </widget> + </item> + </layout> + </item> + <item row="11" column="0"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Auto-Theme Generation</string> + </property> + </widget> + </item> + <item row="11" column="1"> + <widget class="QComboBox" name="auto_theme"/> + </item> + <item row="12" column="0"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>Page Margin</string> + </property> + </widget> + </item> + <item row="12" column="1"> + <widget class="QDoubleSpinBox" name="page_margin"> + <property name="suffix"> + <string> px</string> + </property> + <property name="decimals"> + <number>0</number> + </property> + <property name="maximum"> + <double>350.000000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTextBrowser" name="style_preview"> + <property name="openLinks"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="preview_url"> + <property name="placeholderText"> + <string>host.name</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="generic"> + <attribute name="title"> + <string>Generic</string> + </attribute> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="icons.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SettingsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>325</x> + <y>470</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SettingsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>257</x> + <y>470</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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 <QAbstractListModel> +#include <QVector> +#include <QUrl> + +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<QUrl> history; +}; + +#endif // TABBROWSINGHISTORY_HPP |
