kristall/src/browsertab.cpp

597 lines
18 KiB
C++

#include "browsertab.hpp"
#include "ui_browsertab.h"
#include "mainwindow.hpp"
#include "geminirenderer.hpp"
#include "settingsdialog.hpp"
#include "gophermaprenderer.hpp"
#include "ioutil.hpp"
#include "kristall.hpp"
#include <cassert>
#include <QTabWidget>
#include <QMenu>
#include <QMessageBox>
#include <QInputDialog>
#include <QDockWidget>
#include <QImage>
#include <QPixmap>
#include <QFile>
#include <QMimeDatabase>
#include <QMimeType>
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
BrowserTab::BrowserTab(MainWindow * mainWindow) :
QWidget(nullptr),
ui(new Ui::BrowserTab),
mainWindow(mainWindow),
outline(),
graphics_scene()
{
ui->setupUi(this);
connect(&web_client, &WebClient::requestComplete, this, &BrowserTab::on_requestComplete);
connect(&web_client, &WebClient::requestFailed, this, &BrowserTab::on_requestFailed);
connect(&web_client, &WebClient::requestProgress, this, &BrowserTab::on_requestProgress);
connect(&gemini_client, &GeminiClient::requestComplete, this, &BrowserTab::on_requestComplete);
connect(&gemini_client, &GeminiClient::requestProgress, this, &BrowserTab::on_requestProgress);
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);
connect(&gopher_client, &GopherClient::requestComplete, this, &BrowserTab::on_requestComplete);
connect(&gopher_client, &GopherClient::requestFailed, this, &BrowserTab::on_requestFailed);
connect(&gopher_client, &GopherClient::requestProgress, this, &BrowserTab::on_requestProgress);
connect(&finger_client, &FingerClient::requestComplete, this, &BrowserTab::on_requestComplete);
connect(&finger_client, &FingerClient::requestFailed, this, &BrowserTab::on_requestFailed);
connect(&finger_client, &FingerClient::requestProgress, this, &BrowserTab::on_requestProgress);
this->updateUI();
this->ui->media_browser->setVisible(false);
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)
{
if(not mainWindow->protocols.isSchemeSupported(url.scheme()))
{
QMessageBox::warning(this, "Kristall", "Unsupported uri scheme: " + url.scheme());
return;
}
this->timer.start();
this->current_location = url;
this->ui->url_bar->setText(url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)));
if(not gemini_client.cancelRequest()) {
QMessageBox::warning(this, "Kristall", "Failed to cancel running gemini request!");
return;
}
if(not web_client.cancelRequest()) {
QMessageBox::warning(this, "Kristall", "Failed to cancel running web request!");
return;
}
if(not gopher_client.cancelRequest()) {
QMessageBox::warning(this, "Kristall", "Failed to cancel running gopher request!");
return;
}
if(not finger_client.cancelRequest()) {
QMessageBox::warning(this, "Kristall", "Failed to cancel running finger request!");
return;
}
this->redirection_count = 0;
this->successfully_loaded = false;
this->push_to_history_after_load = (mode == PushAfterSuccess);
if(url.scheme() == "gemini")
{
gemini_client.startRequest(url);
}
else if(url.scheme() == "http" or url.scheme() == "https")
{
web_client.startRequest(url);
}
else if(url.scheme() == "gopher")
{
gopher_client.startRequest(url);
}
else if(url.scheme() == "finger")
{
finger_client.startRequest(url);
}
else if(url.scheme() == "file")
{
QFile file { url.path() };
if(file.open(QFile::ReadOnly))
{
QMimeDatabase db;
auto mime = db.mimeTypeForUrl(url).name();
auto data = file.readAll();
qDebug() << "database:" << url << mime;
this->on_requestComplete(data, mime);
}
else
{
}
}
else if(url.scheme() == "about")
{
this->redirection_count = 0;
this->push_to_history_after_load = false;
if(mode == PushAfterSuccess)
mode = PushImmediate;
if(url.path() == "blank")
{
this->on_requestComplete("", "text/gemini");
}
else if(url.path() == "favourites")
{
QByteArray document;
document.append("# Favourites\n");
document.append("\n");
for(auto const & fav : this->mainWindow->favourites.getAll())
{
document.append("=> " + fav.toString().toUtf8() + "\n");
}
this->on_requestComplete(document, "text/gemini");
}
else
{
QFile file(QString(":/about/%1.gemini").arg(url.path()));
if(file.open(QFile::ReadOnly))
{
this->on_requestComplete(file.readAll(), "text/gemini");
}
else
{
QMessageBox::warning(this, "Kristall", "Unknown location: " + url.path());
}
}
}
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::toggleIsFavourite()
{
toggleIsFavourite(not this->ui->fav_button->isChecked());
}
void BrowserTab::toggleIsFavourite(bool isFavourite)
{
if(isFavourite) {
this->mainWindow->favourites.add(this->current_location);
} else {
this->mainWindow->favourites.remove(this->current_location);
}
this->updateUI();
}
void BrowserTab::focusUrlBar()
{
this->ui->url_bar->setFocus(Qt::ShortcutFocusReason);
this->ui->url_bar->selectAll();
}
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_requestFailed(const QString &reason)
{
this->setErrorMessage(QString("Request failed:\n%1").arg(reason));
}
void BrowserTab::on_requestComplete(const QByteArray &data, const QString &mime)
{
qDebug() << "Loaded" << data.length() << "bytes of type" << mime;
this->current_mime = mime;
this->current_buffer = data;
this->graphics_scene.clear();
this->ui->text_browser->setText("");
ui->text_browser->setStyleSheet("");
enum DocumentType { Text, Image, Media };
DocumentType doc_type = Text;
std::unique_ptr<QTextDocument> document;
this->outline.clear();
auto doc_style = mainWindow->current_style.derive(this->current_location);
this->ui->text_browser->setStyleSheet(QString("QTextBrowser { background-color: %1; }").arg(doc_style.background_color.name()));
bool plaintext_only = (global_settings.value("text_display").toString() == "plain");
if(not plaintext_only and mime.startsWith("text/gemini")) {
document = GeminiRenderer::render(
data,
this->current_location,
doc_style,
this->outline);
}
else if(not plaintext_only and mime.startsWith("text/gophermap")) {
document = GophermapRenderer::render(
data,
this->current_location,
doc_style);
}
else if(not plaintext_only and mime.startsWith("text/finger")) {
document = std::make_unique<QTextDocument>();
document->setDefaultFont(doc_style.preformatted_font);
document->setDefaultStyleSheet(doc_style.toStyleSheet());
document->setPlainText(QString::fromUtf8(data));
}
else if(not plaintext_only and mime.startsWith("text/html")) {
document = std::make_unique<QTextDocument>();
document->setDefaultFont(doc_style.standard_font);
document->setDefaultStyleSheet(doc_style.toStyleSheet());
document->setHtml(QString::fromUtf8(data));
}
#if defined(QT_FEATURE_textmarkdownreader)
else if(not plaintext_only and mime.startsWith("text/markdown")) {
document = std::make_unique<QTextDocument>();
document->setDefaultFont(doc_style.standard_font);
document->setDefaultStyleSheet(doc_style.toStyleSheet());
document->setMarkdown(QString::fromUtf8(data));
}
#endif
else if(mime.startsWith("text/")) {
document = std::make_unique<QTextDocument>();
document->setDefaultFont(doc_style.standard_font);
document->setDefaultStyleSheet(doc_style.toStyleSheet());
document->setPlainText(QString::fromUtf8(data));
}
else if(mime.startsWith("image/")) {
doc_type = 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 if(mime.startsWith("video/") or mime.startsWith("audio/")) {
doc_type = Media;
this->ui->media_browser->setMedia(data, this->current_location, mime);
}
else {
document = std::make_unique<QTextDocument>();
document->setDefaultFont(doc_style.standard_font);
document->setDefaultStyleSheet(doc_style.toStyleSheet());
document->setPlainText(QString(R"md(You accessed an unsupported media type!
Use the *File* menu to save the file to your local disk or navigate somewhere else. I cannot display this for you.
Info:
MIME Type: %1
File Size: %2
)md").arg(mime).arg(IoUtil::size_human(data.size())));
}
assert((document != nullptr) == (doc_type == Text));
this->ui->text_browser->setVisible(doc_type == Text);
this->ui->graphics_browser->setVisible(doc_type == Image);
this->ui->media_browser->setVisible(doc_type == Media);
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);
emit this->fileLoaded(data.size(), mime, this->timer.elapsed());
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()
{
toggleIsFavourite(this->ui->fav_button->isChecked());
}
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(not mainWindow->protocols.isSchemeSupported(real_url.scheme())) {
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)
{
if(url.isValid()) {
QUrl real_url = url;
if(real_url.isRelative())
real_url = this->current_location.resolved(url);
this->mainWindow->setUrlPreview(real_url);
}
else {
this->mainWindow->setUrlPreview(QUrl { });
}
}
void BrowserTab::on_stop_button_clicked()
{
gemini_client.cancelRequest();
web_client.cancelRequest();
gopher_client.cancelRequest();
finger_client.cancelRequest();
}
void BrowserTab::on_requestProgress(qint64 transferred)
{
emit this->fileLoaded(transferred, "Loading...", timer.elapsed());
}
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));
}