#include "mainwindow.hpp" #include "ui_mainwindow.h" #include "browsertab.hpp" #include "dialogs/settingsdialog.hpp" #include #include #include #include #include #include #include #include #include #include #include "ioutil.hpp" #include "kristall.hpp" #include "widgets/browsertabbar.hpp" #include "dialogs/certificatemanagementdialog.hpp" MainWindow::MainWindow(QApplication * app, QWidget *parent) : QMainWindow(parent), application(app), ui(new Ui::MainWindow), url_status(new ElideLabel(this)), file_size(new QLabel(this)), file_cached(new QLabel(this)), file_mime(new QLabel(this)), load_time(new QLabel(this)) { this->setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); connect( // connect with "this" as context, so the connection will die when the window is destroyed kristall::globals().localization.get(), &Localization::translationChanged, this, [this]() { this->ui->retranslateUi(this); }, Qt::DirectConnection ); this->url_status->setElideMode(Qt::ElideMiddle); this->statusBar()->addWidget(this->url_status); this->statusBar()->addPermanentWidget(this->file_cached); this->statusBar()->addPermanentWidget(this->file_mime); this->statusBar()->addPermanentWidget(this->file_size); this->statusBar()->addPermanentWidget(this->load_time); ui->favourites_view->setModel(&kristall::globals().favourites); this->ui->outline_window->setVisible(false); this->ui->history_window->setVisible(false); this->ui->bookmarks_window->setVisible(false); for(QDockWidget * dock : findChildren()) { QAction * act = dock->toggleViewAction(); act->setShortcut(dock->property("_shortcut").toString()); // act->setIcon(dock->windowIcon()); this->ui->menuView->addAction(act); } connect(this->ui->menuNavigation, &QMenu::aboutToShow, [this]() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { ui->actionAdd_to_favourites->setChecked(kristall::globals().favourites.containsUrl(tab->current_location)); } }); connect(this->ui->menuView, &QMenu::aboutToShow, [this]() { for(QAction * act : this->ui->menuView->actions()) { auto * dock = qvariant_cast(act->data()); if(dock != nullptr) { act->setChecked(dock->isVisible()); } } }); { QShortcut * sc = new QShortcut(QKeySequence("Ctrl+L"), this); connect(sc, &QShortcut::activated, this, &MainWindow::on_focus_inputbar); } { std::string prefix = "Alt+"; for (char tab = '0'; tab <= '9'; ++tab) { std::string shortcut = prefix + tab; QShortcut * sc = new QShortcut(QKeySequence(shortcut.c_str()), this); connect(sc, &QShortcut::activated, this, [this, tab]() { // 1-9 goes from the first to the n-th tab, 0 goes to the last one setCurrentTabIndex((tab == '0' ? this->ui->browser_tabs->count() : tab-'0') - 1); }); } } { QShortcut * sc = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageDown), this); connect(sc, &QShortcut::activated, this, [this](){ int i = this->currentTabIndex(); if (i + 1 >= this->ui->browser_tabs->count()) i = 0; else i++; this->setCurrentTabIndex(i); }); } { QShortcut * sc = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageUp), this); connect(sc, &QShortcut::activated, this, [this](){ int i = this->currentTabIndex(); if (!i) i = this->ui->browser_tabs->count() - 1; else i--; this->setCurrentTabIndex(i); }); } this->ui->favourites_view->setContextMenuPolicy(Qt::CustomContextMenu); this->ui->history_view->setContextMenuPolicy(Qt::CustomContextMenu); connect(this->ui->browser_tabs->tab_bar, &BrowserTabBar::on_newTabClicked, this, [this]() { this->addEmptyTab(true, true); }); kristall::registerAppWindow(this); } MainWindow::~MainWindow() { delete ui; } BrowserTab * MainWindow::addEmptyTab(bool focus_new, bool load_default) { BrowserTab * tab = new BrowserTab(this); connect(tab, &BrowserTab::destroyed, this, &MainWindow::on_tab_closed); connect(tab, &BrowserTab::titleChanged, this, &MainWindow::on_tab_titleChanged); connect(tab, &BrowserTab::fileLoaded, this, &MainWindow::on_tab_fileLoaded); connect(tab, &BrowserTab::requestStateChanged, this, &MainWindow::on_tab_requestStateChanged); int index = this->ui->browser_tabs->addTab(tab, "Page"); if(focus_new) { this->setCurrentTabIndex(index); } if(load_default) { tab->navigateTo(QUrl(kristall::globals().options.start_page), BrowserTab::PushImmediate); tab->focusUrlBar(); } else { tab->navigateTo(QUrl("about:blank"), BrowserTab::DontPush); } return tab; } BrowserTab * MainWindow::addNewTab(bool focus_new, QUrl const & url, bool lazyload, QString defaultTitle) { auto tab = addEmptyTab(focus_new, false); if (lazyload) { tab->current_location = url; tab->lazy_loading = true; } tab->navigateTo(url, BrowserTab::PushImmediate); if (!defaultTitle.isEmpty()) { tab->page_title = defaultTitle; emit tab->titleChanged(defaultTitle); } return tab; } BrowserTab * MainWindow::curTab() const { // Was getting irritated writing this out all the time return qobject_cast(this->ui->browser_tabs->currentWidget()); } BrowserTab * MainWindow::tabAt(int index) const { return qobject_cast(this->ui->browser_tabs->widget(index)); } int MainWindow::tabCount() const { return this->ui->browser_tabs->count(); } void MainWindow::setUrlPreview(const QUrl &url) { if(url.isValid()) { auto str = url.toString(); if(str.length() > 300) { str = str.mid(0, 300) + "..."; } this->previewing_url = true; this->url_status->setText(str); return; } this->previewing_url = false; this->url_status->setText(this->request_status); } void MainWindow::setRequestState(RequestState state) { switch (state) { case RequestState::Started: { this->request_status = tr("Looking up..."); } break; case RequestState::StartedWeb: { this->request_status = tr("Loading webpage..."); } break; case RequestState::HostFound: { this->request_status = tr("Connecting..."); } break; case RequestState::Connected: { this->request_status = tr("Downloading..."); } break; default: { this->request_status = ""; } break; } if (!this->previewing_url) this->url_status->setText(this->request_status); } void MainWindow::viewPageSource() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->openSourceView(); } } void MainWindow::updateWindowTitle() { BrowserTab * tab = this->curTab(); if (tab == nullptr || tab->page_title.isEmpty()) { this->setWindowTitle(tr("Kristall")); return; } this->setWindowTitle(tr("%0 - %1").arg(tab->page_title, tr("Kristall"))); } void MainWindow::setUiDensity(UIDensity density, bool previewing) { // If we are previewing, we only update the current tab. // If not, we update all tabs as it means user accepted the settings // dialog. if (previewing) { if (not this->curTab()) return; this->curTab()->setUiDensity(density); } else { for (int i = 0; i < this->ui->browser_tabs->count(); ++i) this->tabAt(i)->setUiDensity(density); } } QString MainWindow::newGroupDialog() { QInputDialog dialog { this }; dialog.setInputMode(QInputDialog::TextInput); dialog.setLabelText(tr("Enter name of the new group:")); if(dialog.exec() != QDialog::Accepted) return QString { }; kristall::globals().favourites.addGroup(dialog.textValue()); return dialog.textValue(); } void MainWindow::applySettings() { // Flag open tabs for re-render so theme // changes are instantly applied. for (int i = 0; i < this->ui->browser_tabs->count(); ++i) { BrowserTab *t = this->tabAt(i); t->refreshOptionalToolbarItems(); t->refreshToolbarIcons(); t->needs_rerender = true; } // Re-render the currently-open tab if we have one. BrowserTab * tab = this->curTab(); if (tab) tab->rerenderPage(); // Update new-tab button visibility. this->ui->browser_tabs->tab_bar->new_tab_btn->setVisible(kristall::globals().options.enable_newtab_btn); } int MainWindow::currentTabIndex() { return this->ui->browser_tabs->currentIndex(); } void MainWindow::setCurrentTabIndex(int index) { this->ui->browser_tabs->setCurrentIndex(index); } void MainWindow::mousePressEvent(QMouseEvent *event) { QMainWindow::mousePressEvent(event); BrowserTab * tab = this->curTab(); if (tab == nullptr) return; // Navigate back/forward on mouse buttons 4/5 if (event->buttons() == Qt::ForwardButton && tab->history.oneForward(tab->current_history_index).isValid()) { tab->navOneForward(); } else if (event->buttons() == Qt::BackButton && tab->history.oneBackward(tab->current_history_index).isValid()) { tab->navOneBackward(); } } void MainWindow::closeEvent(QCloseEvent *event) { if(kristall::getWindowCount() == 1) { kristall::saveSession(); } event->accept(); } void MainWindow::on_tab_closed() { // If the user wants, we close the window together with the last tab. // tabCount() might be 1 here, as the tab is still counted as "open" if(kristall::globals().options.close_window_with_last_tab and (this->tabCount() <= 1)) { this->close(); } } void MainWindow::on_browser_tabs_currentChanged(int index) { if(index >= 0) { BrowserTab * tab = this->tabAt(index); if(tab != nullptr) { this->ui->outline_view->setModel(&tab->outline); this->ui->outline_view->expandAll(); this->ui->history_view->setModel(&tab->history); this->setFileStatus(tab->current_stats); if (tab->needs_rerender) { tab->rerenderPage(); } else { tab->refreshFavButton(); } if (tab->lazy_loading) { tab->reloadPage(); } this->setRequestState(tab->request_state); } else { this->ui->outline_view->setModel(nullptr); this->ui->history_view->setModel(nullptr); this->setFileStatus(DocumentStats { }); this->setRequestState(RequestState::None); } } else { this->ui->outline_view->setModel(nullptr); this->ui->history_view->setModel(nullptr); this->setFileStatus(DocumentStats { }); this->setRequestState(RequestState::None); } updateWindowTitle(); } void MainWindow::on_browser_tabs_tabCloseRequested(int index) { delete tabAt(index); } void MainWindow::on_tab_titleChanged(const QString &title) { auto * tab = qobject_cast(sender()); if(tab != nullptr) { int index = this->ui->browser_tabs->indexOf(tab); assert(index >= 0); QString escapedTitle = title; // Set the window title to full title if (tab == this->curTab()) { updateWindowTitle(); } // Set tooltip this->ui->browser_tabs->tab_bar->setTabToolTip(index, title); // Shorten lengthy titles for tab bar (45 chars max for now - we assume // that Gemini surfers don't usually have loads of tabs open, so the // limit is fairly high) const int MAX_TITLE_LEN = 45; if (escapedTitle.length() > MAX_TITLE_LEN) { escapedTitle = escapedTitle.mid(0, MAX_TITLE_LEN - 3).trimmed() + "..."; } this->ui->browser_tabs->setTabText(index, escapedTitle.replace("&", "&&")); } } void MainWindow::on_tab_locationChanged(const QUrl &url) { auto * tab = qobject_cast(sender()); if(tab != nullptr) { int index = this->ui->browser_tabs->indexOf(tab); assert(index >= 0); this->ui->browser_tabs->setTabToolTip(index, url.toString()); } } void MainWindow::on_outline_view_clicked(const QModelIndex &index) { BrowserTab * tab = this->curTab(); 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(kristall::globals().document_style); dialog.setProtocols(kristall::globals().protocols); dialog.setOptions(kristall::globals().options); dialog.setGeminiSslTrust(kristall::globals().trust.gemini); dialog.setHttpsSslTrust(kristall::globals().trust.https); dialog.setLocale(kristall::globals().localization->locale); if(dialog.exec() != QDialog::Accepted) { kristall::setTheme(kristall::globals().options.theme); kristall::globals().localization->setLocale(kristall::globals().localization->locale); this->setUiDensity(kristall::globals().options.ui_density, false); return; } kristall::globals().localization->setLocale(dialog.locale()); kristall::globals().trust.gemini = dialog.geminiSslTrust(); kristall::globals().trust.https = dialog.httpsSslTrust(); kristall::globals().options = dialog.options(); kristall::globals().protocols = dialog.protocols(); kristall::globals().document_style = dialog.geminiStyle(); kristall::saveLocale(); kristall::applySettings(); kristall::saveSettings(); } void MainWindow::on_actionNew_Tab_triggered() { this->addEmptyTab(true, true); } void MainWindow::on_actionQuit_triggered() { kristall::saveSession(); QApplication::quit(); } void MainWindow::on_actionAbout_triggered() { QMessageBox::about(this, tr("Kristall"), tr(R"about(Kristall %1 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").arg(QApplication::applicationVersion()) ); } void MainWindow::on_actionClose_Tab_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { delete tab; } } void MainWindow::on_actionForward_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navOneForward(); } } void MainWindow::on_actionBackward_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navOneBackward(); } } void MainWindow::on_actionRoot_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navigateToRoot(); } } void MainWindow::on_actionParent_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navigateToParent(); } } void MainWindow::on_actionRefresh_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->reloadPage(); } } void MainWindow::on_actionAbout_Qt_triggered() { QMessageBox::aboutQt(this, "Kristall"); } void MainWindow::setFileStatus(const DocumentStats &stats) { if(stats.isValid()) { this->file_size->setText(IoUtil::size_human(stats.file_size)); this->file_cached->setText(stats.loaded_from_cache ? tr("(cached)") : ""); this->file_mime->setText(stats.mime_type.toString(false)); this->load_time->setText(tr("%1 ms").arg(stats.loading_time)); } else { this->file_size->setText(""); this->file_cached->setText(""); this->file_mime->setText(""); this->load_time->setText(""); } } void MainWindow::on_actionSave_as_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { QFileDialog dialog { this }; dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.selectFile(tab->current_location.fileName()); if(dialog.exec() !=QFileDialog::Accepted) return; QString fileName = dialog.selectedFiles().at(0); QFile file { fileName }; if(file.open(QFile::WriteOnly)) { IoUtil::writeAll(file, tab->current_buffer); } else { QMessageBox::warning(this, tr("Kristall"), QString("Could not save file:\r\n%1").arg(file.errorString())); } } } void MainWindow::on_actionGo_to_home_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navigateTo(QUrl(kristall::globals().options.start_page), BrowserTab::PushImmediate); } } void MainWindow::on_actionAdd_to_favourites_triggered() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->showFavouritesPopup(); } } void MainWindow::on_tab_fileLoaded(DocumentStats const & stats) { auto * tab = qobject_cast(sender()); if(tab != nullptr) { int index = this->ui->browser_tabs->indexOf(tab); assert(index >= 0); if(index == this->currentTabIndex()) { setFileStatus(stats); this->ui->outline_view->expandAll(); } } } void MainWindow::on_tab_requestStateChanged(RequestState state) { auto * tab = qobject_cast(sender()); if(tab != nullptr) { int index = this->ui->browser_tabs->indexOf(tab); assert(index >= 0); if(index == this->currentTabIndex()) { setRequestState(state); } } } void MainWindow::on_focus_inputbar() { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->focusUrlBar(); } } void MainWindow::on_actionHelp_triggered() { this->addNewTab(true, QUrl("about:help")); } void MainWindow::on_history_view_customContextMenuRequested(const QPoint pos) { if(auto idx = this->ui->history_view->indexAt(pos); idx.isValid()) { BrowserTab * tab = this->curTab(); if(tab != nullptr) { if(QUrl url = tab->history.get(idx); url.isValid()) { QMenu menu; connect(menu.addAction(tr("Open here")), &QAction::triggered, [tab, idx]() { // We do the same thing as a double click here tab->navigateBack(idx); }); connect(menu.addAction(tr("Open in new tab")), &QAction::triggered, [this, url]() { addNewTab(true, url); }); menu.exec(this->ui->history_view->mapToGlobal(pos)); } } } } void MainWindow::on_favourites_view_customContextMenuRequested(const QPoint pos) { if(auto idx = this->ui->favourites_view->indexAt(pos); idx.isValid()) { if(QUrl url = kristall::globals().favourites.getFavourite(idx).destination; url.isValid()) { QMenu menu; BrowserTab * tab = this->curTab(); if(tab != nullptr) { connect(menu.addAction(tr("Open here")), &QAction::triggered, [tab, url]() { tab->navigateTo(url, BrowserTab::PushImmediate); }); } connect(menu.addAction(tr("Open in new tab")), &QAction::triggered, [this, url]() { addNewTab(true, url); }); menu.addSeparator(); connect(menu.addAction(tr("Relocate")), &QAction::triggered, [this, idx]() { QInputDialog dialog { this }; dialog.setInputMode(QInputDialog::TextInput); dialog.setLabelText(tr("Enter new location of this favourite:")); dialog.setTextValue(kristall::globals().favourites.getFavourite(idx).destination.toString(QUrl::FullyEncoded)); if (dialog.exec() != QDialog::Accepted) return; kristall::globals().favourites.editFavouriteDest(idx, QUrl(dialog.textValue())); }); connect(menu.addAction(tr("Rename")), &QAction::triggered, [this, idx]() { QInputDialog dialog { this }; dialog.setInputMode(QInputDialog::TextInput); dialog.setLabelText(tr("New name of this favourite:")); dialog.setTextValue(kristall::globals().favourites.getFavourite(idx).getTitle()); if (dialog.exec() != QDialog::Accepted) return; kristall::globals().favourites.editFavouriteTitle(idx, dialog.textValue()); }); menu.addSeparator(); connect(menu.addAction(tr("Delete")), &QAction::triggered, [idx]() { kristall::globals().favourites.destroyFavourite(idx); }); menu.exec(this->ui->favourites_view->mapToGlobal(pos)); } else if(QString group = kristall::globals().favourites.group(idx); not group.isEmpty()) { QMenu menu; connect(menu.addAction(tr("Rename group")), &QAction::triggered, [this, group]() { QInputDialog dialog { this }; dialog.setInputMode(QInputDialog::TextInput); dialog.setLabelText(tr("New name of this group:")); dialog.setTextValue(group); if (dialog.exec() != QDialog::Accepted) return; if (!kristall::globals().favourites.renameGroup(group, dialog.textValue())) QMessageBox::information(this, tr("Kristall"), tr("Rename failed: group name already in use.")); }); menu.addSeparator(); connect(menu.addAction(tr("Delete group")), &QAction::triggered, [this, idx]() { if (QMessageBox::question( this, tr("Kristall"), tr("Are you sure you want to delete this Favourite Group?\n" "All favourites in this group will be lost.\n\n" "This action cannot be undone!") ) != QMessageBox::Yes) { return; } kristall::globals().favourites.deleteGroupRecursive(kristall::globals().favourites.group(idx)); }); menu.exec(this->ui->favourites_view->mapToGlobal(pos)); } } else { QMenu menu; connect(menu.addAction(tr("Create new group...")), &QAction::triggered, [this]() { this->newGroupDialog(); }); menu.exec(this->ui->favourites_view->mapToGlobal(pos)); } } void MainWindow::on_actionChangelog_triggered() { this->addNewTab(true, QUrl("about:updates")); } void MainWindow::on_actionManage_Certificates_triggered() { CertificateManagementDialog dialog { this }; dialog.setIdentitySet(kristall::globals().identities); if(dialog.exec() != QDialog::Accepted) return; kristall::globals().identities = dialog.identitySet(); kristall::saveSettings(); } void MainWindow::on_actionShow_document_source_triggered() { this->viewPageSource(); } void MainWindow::on_actionNew_window_triggered() { kristall::openNewWindow(false); } void MainWindow::on_actionClose_Window_triggered() { this->close(); } void MainWindow::on_favourites_view_activated(const QModelIndex &index) { if(auto url = kristall::globals().favourites.getFavourite(index).destination; url.isValid()) { this->addNewTab(true, url); } } void MainWindow::on_history_view_activated(const QModelIndex &index) { BrowserTab * tab = this->curTab(); if(tab != nullptr) { tab->navigateBack(index); } } void MainWindow::on_outline_view_activated(const QModelIndex &index) { this->on_outline_view_clicked(index); }