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"