File indexing completed on 2024-05-12 16:23:41

0001 /*
0002  *  SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert <jbb@kaidan.im>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-only
0005  */
0006 
0007 #include "tabsmodel.h"
0008 
0009 #include <QDebug>
0010 #include <QDir>
0011 #include <QFile>
0012 #include <QJsonArray>
0013 #include <QJsonDocument>
0014 #include <QJsonObject>
0015 #include <QStandardPaths>
0016 #include <QUrl>
0017 
0018 #include <ranges>
0019 
0020 #include "angelfishsettings.h"
0021 #include "browsermanager.h"
0022 
0023 namespace ranges = std::ranges;
0024 
0025 TabsModel::TabsModel(QObject *parent)
0026     : QAbstractListModel(parent)
0027 {
0028     connect(this, &TabsModel::currentTabChanged, [this] {
0029         qDebug() << "Current tab changed to" << m_currentTab;
0030     });
0031 
0032     // The fallback tab must not be saved, it would overwrite our actual data.
0033     m_tabsReadOnly = true;
0034     // Make sure model always contains at least one tab
0035     createEmptyTab();
0036 
0037     // Only load tabs after private mode is known
0038     connect(this, &TabsModel::privateModeChanged, [this] {
0039         loadInitialTabs();
0040     });
0041 }
0042 
0043 QHash<int, QByteArray> TabsModel::roleNames() const
0044 {
0045     return {
0046         {RoleNames::UrlRole, QByteArrayLiteral("pageurl")},
0047         {RoleNames::IsMobileRole, QByteArrayLiteral("isMobile")},
0048         {RoleNames::IsDeveloperToolsOpen, QByteArrayLiteral("isDeveloperToolsOpen")},
0049     };
0050 }
0051 
0052 QVariant TabsModel::data(const QModelIndex &index, int role) const
0053 {
0054     if (!index.isValid() || index.row() < 0 || size_t(index.row()) >= m_tabs.size()) {
0055         return {};
0056     }
0057 
0058     switch (role) {
0059     case RoleNames::UrlRole:
0060         return m_tabs.at(index.row()).url();
0061     case RoleNames::IsMobileRole:
0062         return m_tabs.at(index.row()).isMobile();
0063     case RoleNames::IsDeveloperToolsOpen:
0064         return m_tabs.at(index.row()).isDeveloperToolsOpen();
0065     }
0066 
0067     return {};
0068 }
0069 
0070 int TabsModel::rowCount(const QModelIndex &parent) const
0071 {
0072     return parent.isValid() ? 0 : int(m_tabs.size());
0073 }
0074 
0075 /**
0076  * @brief TabsModel::tab returns the tab at the given index
0077  * @param index
0078  * @return tab at the index
0079  */
0080 TabState TabsModel::tab(int index)
0081 {
0082     if (index < 0 || size_t(index) >= m_tabs.size())
0083         return {}; // index out of bounds
0084 
0085     return m_tabs.at(index);
0086 }
0087 
0088 /**
0089  * @brief TabsModel::loadInitialTabs sets up the tabs that should already be open when starting the browser
0090  * This includes the configured homepage, an url passed on the command line (usually by another app) and tabs
0091  * which were still open when the browser was last closed.
0092  *
0093  * @warning It is impossible to save any new tabs until this function was called.
0094  */
0095 void TabsModel::loadInitialTabs()
0096 {
0097     if (m_initialTabsLoaded) {
0098         return;
0099     }
0100 
0101     if (!m_privateMode) {
0102         loadTabs();
0103     }
0104 
0105     m_tabsReadOnly = false;
0106 
0107     if (!m_privateMode) {
0108         if (BrowserManager::instance()->initialUrl().isEmpty()) {
0109             if (m_tabs.front().url() == QUrl(QStringLiteral("about:blank")))
0110                 setUrl(0, AngelfishSettings::self()->homepage());
0111         } else {
0112             if (m_tabs.front().url() == QUrl(QStringLiteral("about:blank")))
0113                 setUrl(0, BrowserManager::instance()->initialUrl());
0114             else
0115                 newTab(BrowserManager::instance()->initialUrl());
0116         }
0117     }
0118 
0119     m_initialTabsLoaded = true;
0120 }
0121 
0122 /**
0123  * @brief TabsModel::currentTab returns the index of the tab that is currently visible to the user
0124  * @return index
0125  */
0126 int TabsModel::currentTab() const
0127 {
0128     return m_currentTab;
0129 }
0130 
0131 /**
0132  * @brief TabsModel::setCurrentTab sets the tab that is currently visible to the user
0133  * @param index
0134  */
0135 void TabsModel::setCurrentTab(int index)
0136 {
0137     if (index < 0 || size_t(index) >= m_tabs.size())
0138         return;
0139 
0140     m_currentTab = index;
0141     Q_EMIT currentTabChanged();
0142     saveTabs();
0143 }
0144 
0145 const std::vector<TabState> &TabsModel::tabs() const
0146 {
0147     return m_tabs;
0148 }
0149 
0150 /**
0151  * @brief TabsModel::loadTabs restores tabs saved in tabs.json
0152  * @return whether any tabs were restored
0153  */
0154 bool TabsModel::loadTabs()
0155 {
0156     if (!m_privateMode) {
0157         beginResetModel();
0158         const QString input = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/angelfish/tabs.json");
0159 
0160         QFile inputFile(input);
0161         if (!inputFile.exists()) {
0162             return false;
0163         }
0164 
0165         if (!inputFile.open(QIODevice::ReadOnly)) {
0166             qDebug() << "Failed to load tabs from disk";
0167         }
0168 
0169         const auto tabsStorage = QJsonDocument::fromJson(inputFile.readAll()).object();
0170         m_tabs.clear();
0171 
0172         const auto tabs = tabsStorage.value(QLatin1String("tabs")).toArray();
0173 
0174         ranges::transform(tabs, std::back_inserter(m_tabs), [](const QJsonValue &tab) {
0175             return TabState::fromJson(tab.toObject());
0176         });
0177 
0178         qDebug() << "loaded from file:" << m_tabs.size() << input;
0179 
0180         m_currentTab = tabsStorage.value(QLatin1String("currentTab")).toInt();
0181 
0182         // Make sure model always contains at least one tab
0183         if (m_tabs.size() == 0) {
0184             createEmptyTab();
0185         }
0186 
0187         endResetModel();
0188         Q_EMIT currentTabChanged();
0189 
0190         return true;
0191     }
0192     return false;
0193 }
0194 
0195 /**
0196  * @brief TabsModel::saveTabs saves the current state of the model to disk
0197  * @return whether the tabs could be saved
0198  */
0199 bool TabsModel::saveTabs() const
0200 {
0201     // only save if not in private mode
0202     if (!m_privateMode && !m_tabsReadOnly) {
0203         const QString outputDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/angelfish/");
0204 
0205         QFile outputFile(outputDir + QStringLiteral("tabs.json"));
0206         if (!QDir(outputDir).mkpath(QStringLiteral("."))) {
0207             qDebug() << "Destdir doesn't exist and I can't create it: " << outputDir;
0208             return false;
0209         }
0210         if (!outputFile.open(QIODevice::WriteOnly)) {
0211             qDebug() << "Failed to write tabs to disk";
0212         }
0213 
0214         QJsonArray tabsArray;
0215         ranges::transform(m_tabs, std::back_inserter(tabsArray), [](const TabState &tab) {
0216             return tab.toJson();
0217         });
0218 
0219         qDebug() << "Wrote to file" << outputFile.fileName() << "(" << tabsArray.count() << "urls"
0220                  << ")";
0221 
0222         const QJsonDocument document({
0223             {QLatin1String("tabs"), tabsArray},
0224             {QLatin1String("currentTab"), m_currentTab},
0225         });
0226 
0227         outputFile.write(document.toJson());
0228         return true;
0229     }
0230     return false;
0231 }
0232 
0233 bool TabsModel::isMobileDefault() const
0234 {
0235     return m_isMobileDefault;
0236 }
0237 
0238 void TabsModel::setIsMobileDefault(bool def)
0239 {
0240     if (m_isMobileDefault != def) {
0241         m_isMobileDefault = def;
0242         Q_EMIT isMobileDefaultChanged();
0243 
0244         // used in initialization of the tab
0245         if (m_tabs.size() == 1) {
0246             setIsMobile(0, def);
0247         }
0248     }
0249 }
0250 
0251 bool TabsModel::privateMode() const
0252 {
0253     return m_privateMode;
0254 }
0255 
0256 void TabsModel::setPrivateMode(bool privateMode)
0257 {
0258     m_privateMode = privateMode;
0259     Q_EMIT privateModeChanged();
0260 }
0261 
0262 /**
0263  * @brief TabsModel::createEmptyTab convinience function for opening a tab containing "about:blank"
0264  */
0265 void TabsModel::createEmptyTab()
0266 {
0267     newTab(QUrl(QStringLiteral("about:blank")));
0268 };
0269 
0270 /**
0271  * @brief TabsModel::newTab
0272  * @param url
0273  * @param isMobile
0274  */
0275 void TabsModel::newTab(const QUrl &url)
0276 {
0277     beginInsertRows({}, m_tabs.size(), m_tabs.size());
0278 
0279     m_tabs.push_back(TabState(url, m_isMobileDefault));
0280 
0281     endInsertRows();
0282 
0283     // Switch to last tab
0284     if (AngelfishSettings::self()->switchToNewTab()) {
0285         m_currentTab = m_tabs.size() - 1;
0286         Q_EMIT currentTabChanged();
0287     }
0288     saveTabs();
0289 }
0290 
0291 /**
0292  * @brief TabsModel::closeTab removes the tab at the index, handles moving the tabs after it and sets a new currentTab
0293  * @param index
0294  */
0295 void TabsModel::closeTab(int index)
0296 {
0297     if (index < 0 || size_t(index) >= m_tabs.size())
0298         return; // index out of bounds
0299 
0300     if (m_tabs.size() <= 1) {
0301         // if not in mobile, close application
0302         if (!SettingsHelper::isMobile()) {
0303             QCoreApplication::quit();
0304         }
0305 
0306         // create new tab before removing the last one
0307         // to avoid linking all signals to null object
0308         createEmptyTab();
0309 
0310         // now we have (tab_to_remove, "about:blank)
0311 
0312         // 0 will be the correct current tab index after tab_to_remove is gone
0313         m_currentTab = 0;
0314 
0315         // index to remove
0316         index = 0;
0317     }
0318 
0319     if (m_currentTab > index) {
0320         // decrease index if it's after the removed tab
0321         m_currentTab--;
0322     }
0323 
0324     if (m_currentTab == index) {
0325         // handle the removal of current tab
0326         // Just reset to first tab
0327         if (index != 0) {
0328             m_currentTab = index - 1;
0329         } else {
0330             m_currentTab = 0;
0331         }
0332     }
0333 
0334     beginRemoveRows({}, index, index);
0335     m_tabs.erase(m_tabs.begin() + index);
0336     endRemoveRows();
0337 
0338     Q_EMIT currentTabChanged();
0339     saveTabs();
0340 }
0341 
0342 void TabsModel::setIsMobile(int index, bool isMobile)
0343 {
0344     qDebug() << "Setting isMobile:" << index << isMobile << "tabs open" << m_tabs.size();
0345     if (index < 0 || size_t(index) >= m_tabs.size())
0346         return; // index out of bounds
0347 
0348     m_tabs[index].setIsMobile(isMobile);
0349 
0350     const QModelIndex mindex = createIndex(index, index);
0351     Q_EMIT dataChanged(mindex, mindex, {RoleNames::IsMobileRole});
0352     saveTabs();
0353 }
0354 
0355 void TabsModel::toggleDeveloperTools(int index)
0356 {
0357     if (index < 0 || size_t(index) >= m_tabs.size())
0358         return; // index out of bounds
0359 
0360     auto &tab = m_tabs[index];
0361     tab.setIsDeveloperToolsOpen(!tab.isDeveloperToolsOpen());
0362 
0363     const QModelIndex mindex = createIndex(index, index);
0364     Q_EMIT dataChanged(mindex, mindex, {RoleNames::IsDeveloperToolsOpen});
0365     saveTabs();
0366 }
0367 
0368 bool TabsModel::isDeveloperToolsOpen(int index)
0369 {
0370     if (index < 0 || size_t(index) >= m_tabs.size())
0371         return false;
0372 
0373     return m_tabs.at(index).isDeveloperToolsOpen();
0374 }
0375 
0376 void TabsModel::setUrl(int index, const QUrl &url)
0377 {
0378     qDebug() << "Setting URL:" << index << url << "tabs open" << m_tabs.size();
0379     if (index < 0 || size_t(index) >= m_tabs.size())
0380         return; // index out of bounds
0381 
0382     m_tabs[index].setUrl(url);
0383 
0384     const QModelIndex mindex = createIndex(index, index);
0385     Q_EMIT dataChanged(mindex, mindex, {RoleNames::UrlRole});
0386     saveTabs();
0387 }
0388 
0389 QUrl TabState::url() const
0390 {
0391     return m_url;
0392 }
0393 
0394 void TabState::setUrl(const QUrl &url)
0395 {
0396     m_url = url;
0397 }
0398 
0399 bool TabState::isMobile() const
0400 {
0401     return m_isMobile;
0402 }
0403 
0404 void TabState::setIsMobile(bool isMobile)
0405 {
0406     m_isMobile = isMobile;
0407 }
0408 
0409 bool TabState::isDeveloperToolsOpen() const
0410 {
0411     return m_isDeveloperToolsOpen;
0412 }
0413 
0414 void TabState::setIsDeveloperToolsOpen(bool isDeveloperToolsOpen)
0415 {
0416     m_isDeveloperToolsOpen = isDeveloperToolsOpen;
0417 }
0418 
0419 TabState TabState::fromJson(const QJsonObject &obj)
0420 {
0421     TabState tab;
0422     tab.setUrl(QUrl(obj.value(QStringLiteral("url")).toString()));
0423     tab.setIsMobile(obj.value(QStringLiteral("isMobile")).toBool());
0424     tab.setIsDeveloperToolsOpen(obj.value(QStringLiteral("isDeveloperToolsOpen")).toBool());
0425     return tab;
0426 }
0427 
0428 TabState::TabState(const QUrl &url, const bool isMobile)
0429 {
0430     setIsMobile(isMobile);
0431     setUrl(url);
0432 }
0433 
0434 bool TabState::operator==(const TabState &other) const
0435 {
0436     return (
0437         m_url == other.url() &&
0438         m_isMobile == other.isMobile() &&
0439         m_isDeveloperToolsOpen == other.isDeveloperToolsOpen()
0440     );
0441 }
0442 
0443 QJsonObject TabState::toJson() const
0444 {
0445     return {
0446         {QStringLiteral("url"), m_url.toString()},
0447         {QStringLiteral("isMobile"), m_isMobile},
0448         {QStringLiteral("isDeveloperToolsOpen"), m_isDeveloperToolsOpen},
0449     };
0450 }
0451 
0452 #include "moc_tabsmodel.cpp"