diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c2b1dd1..e6878a8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,6 +115,8 @@ set(SQLB_MOC_HDR src/RemoteDatabase.h src/ForeignKeyEditorDelegate.h src/PlotDock.h + src/RemoteDock.h + src/RemoteModel.h ) set(SQLB_SRC @@ -150,6 +152,8 @@ set(SQLB_SRC src/RemoteDatabase.cpp src/ForeignKeyEditorDelegate.cpp src/PlotDock.cpp + src/RemoteDock.cpp + src/RemoteModel.cpp ) set(SQLB_FORMS @@ -167,6 +171,7 @@ set(SQLB_FORMS src/ExportSqlDialog.ui src/ColumnDisplayFormatDialog.ui src/PlotDock.ui + src/RemoteDock.ui ) set(SQLB_RESOURCES diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a707e7ac..e89d101b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -21,6 +21,7 @@ #include "FileDialog.h" #include "ColumnDisplayFormatDialog.h" #include "FilterTableHeader.h" +#include "RemoteDock.h" #include #include @@ -55,8 +56,10 @@ MainWindow::MainWindow(QWidget* parent) ui(new Ui::MainWindow), m_browseTableModel(new SqliteTableModel(db, this, Settings::getSettingsValue("db", "prefetchsize").toInt())), m_currentTabTableModel(m_browseTableModel), + m_remoteDb(new RemoteDatabase), editDock(new EditDialog(this)), plotDock(new PlotDock(this)), + remoteDock(new RemoteDock(this)), gotoValidator(new QIntValidator(0, 0, this)) { ui->setupUi(this); @@ -68,6 +71,7 @@ MainWindow::MainWindow(QWidget* parent) MainWindow::~MainWindow() { + delete m_remoteDb; delete gotoValidator; delete ui; } @@ -77,6 +81,7 @@ void MainWindow::init() // Load window settings tabifyDockWidget(ui->dockLog, ui->dockPlot); tabifyDockWidget(ui->dockLog, ui->dockSchema); + tabifyDockWidget(ui->dockLog, ui->dockRemote); // Connect SQL logging and database state setting to main window connect(&db, SIGNAL(dbChanged(bool)), this, SLOT(dbState(bool))); @@ -107,6 +112,7 @@ void MainWindow::init() // Create docks ui->dockEdit->setWidget(editDock); ui->dockPlot->setWidget(plotDock); + ui->dockRemote->setWidget(remoteDock); // Restore window geometry restoreGeometry(Settings::getSettingsValue("MainWindow", "geometry").toByteArray()); @@ -183,10 +189,12 @@ void MainWindow::init() // Add menu item for edit dock ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockEdit->toggleViewAction()); + ui->viewMenu->actions().at(3)->setShortcut(QKeySequence(tr("Ctrl+E"))); ui->viewMenu->actions().at(3)->setIcon(QIcon(":/icons/log_dock")); - // Add keyboard shortcut for "Edit Cell" dock - ui->viewMenu->actions().at(3)->setShortcut(QKeySequence(tr("Ctrl+E"))); + // Add menu item for plot dock + ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockRemote->toggleViewAction()); + ui->viewMenu->actions().at(4)->setIcon(QIcon(":/icons/log_dock")); // If we're not compiling in SQLCipher, hide its FAQ link in the help menu #ifndef ENABLE_SQLCIPHER @@ -223,7 +231,7 @@ void MainWindow::init() connect(ui->dataTable->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showDataColumnPopupMenu(QPoint))); connect(ui->dataTable->verticalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showRecordPopupMenu(QPoint))); connect(ui->dockEdit, SIGNAL(visibilityChanged(bool)), this, SLOT(toggleEditDock(bool))); - connect(&m_remoteDb, SIGNAL(openFile(QString)), this, SLOT(fileOpen(QString))); + connect(m_remoteDb, SIGNAL(openFile(QString)), this, SLOT(fileOpen(QString))); // Lambda function for keyboard shortcuts for selecting next/previous table in Browse Data tab connect(ui->dataTable, &ExtendedTableWidget::switchTable, [this](bool next) { @@ -274,6 +282,7 @@ void MainWindow::init() ui->dockLog->setWindowTitle(ui->dockLog->windowTitle().remove('&')); ui->dockPlot->setWindowTitle(ui->dockPlot->windowTitle().remove('&')); ui->dockSchema->setWindowTitle(ui->dockSchema->windowTitle().remove('&')); + ui->dockRemote->setWindowTitle(ui->dockRemote->windowTitle().remove('&')); } bool MainWindow::fileOpen(const QString& fileName, bool dontAddToRecentFiles, bool readOnly) @@ -1717,11 +1726,17 @@ void MainWindow::reloadSettings() populateTable(); // Hide or show the File → Remote menu as needed - QAction *remoteMenuAction = ui->menuRemote->menuAction(); - remoteMenuAction->setVisible(Settings::getSettingsValue("remote", "active").toBool()); + bool showRemoteActions = Settings::getSettingsValue("remote", "active").toBool(); + ui->menuRemote->menuAction()->setVisible(showRemoteActions); + ui->viewMenu->actions().at(4)->setVisible(showRemoteActions); + if(!showRemoteActions) + ui->dockRemote->setHidden(true); // Update the remote database connection settings - m_remoteDb.reloadSettings(); + m_remoteDb->reloadSettings(); + + // Reload remote dock settings + remoteDock->reloadSettings(); } void MainWindow::httpresponse(QNetworkReply *reply) @@ -1801,23 +1816,13 @@ void MainWindow::httpresponse(QNetworkReply *reply) reply->deleteLater(); } -void MainWindow::on_actionOpen_Remote_triggered() -{ - QString url = QInputDialog::getText(this, qApp->applicationName(), tr("Please enter the URL of the database file to open.")); - if(!url.isEmpty()) - { - QStringList certs = Settings::getSettingsValue("remote", "client_certificates").toStringList(); - m_remoteDb.fetchDatabase(url, (certs.size() ? certs.at(0) : "")); - } -} - void MainWindow::on_actionSave_Remote_triggered() { QString url = QInputDialog::getText(this, qApp->applicationName(), tr("Please enter the URL of the database file to save.")); if(!url.isEmpty()) { QStringList certs = Settings::getSettingsValue("remote", "client_certificates").toStringList(); - m_remoteDb.pushDatabase(db.currentFile(), url, (certs.size() ? certs.at(0) : "")); + m_remoteDb->push(db.currentFile(), url, (certs.size() ? certs.at(0) : "")); } } diff --git a/src/MainWindow.h b/src/MainWindow.h index 024cc568..4783c34e 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -18,6 +18,7 @@ class SqliteTableModel; class DbStructureModel; class QNetworkReply; class QNetworkAccessManager; +class RemoteDock; namespace Ui { class MainWindow; @@ -83,7 +84,7 @@ public: ~MainWindow(); DBBrowserDB& getDb() { return db; } - const RemoteDatabase& getRemote() const { return m_remoteDb; } + RemoteDatabase& getRemote() { return *m_remoteDb; } enum Tabs { @@ -137,13 +138,16 @@ private: QMap browseTableSettings; + RemoteDatabase* m_remoteDb; + EditDialog* editDock; PlotDock* plotDock; + RemoteDock* remoteDock; + QIntValidator* gotoValidator; DBBrowserDB db; QString defaultBrowseTableEncoding; - RemoteDatabase m_remoteDb; QNetworkAccessManager* m_NetworkManager; @@ -205,7 +209,6 @@ private slots: void exportTableToJson(); void fileSave(); void fileRevert(); - void on_actionOpen_Remote_triggered(); void on_actionSave_Remote_triggered(); void exportDatabaseToSQL(); void importDatabaseFromSQL(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 30017d3c..646e4c42 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -883,7 +883,6 @@ Remote - @@ -1120,6 +1119,15 @@ + + + &Remote + + + 2 + + + @@ -1737,11 +1745,6 @@ QAction::NoRole - - - Open from Remote - - Save to Remote diff --git a/src/RemoteDatabase.cpp b/src/RemoteDatabase.cpp index d0026264..b6edcd7f 100644 --- a/src/RemoteDatabase.cpp +++ b/src/RemoteDatabase.cpp @@ -86,28 +86,38 @@ void RemoteDatabase::gotReply(QNetworkReply* reply) if(reply->error() != QNetworkReply::NoError) { QMessageBox::warning(0, qApp->applicationName(), - tr("Error opening remote database file from %1.\n%2").arg(reply->url().toString()).arg(reply->errorString())); + tr("Error when connecting to %1.\n%2").arg(reply->url().toString()).arg(reply->errorString())); reply->deleteLater(); return; } - // Ask user where to store the database file - QString saveFileAs = FileDialog::getSaveFileName(0, qApp->applicationName(), FileDialog::getSqlDatabaseFileFilter(), reply->url().fileName()); - if(!saveFileAs.isEmpty()) - { - // Save the downloaded data under the selected file name - QFile file(saveFileAs); - file.open(QIODevice::WriteOnly); - file.write(reply->readAll()); - file.close(); + // What type of data is this? + QString type = reply->property("type").toString(); - // Tell the application to open this file - emit openFile(saveFileAs); + // Handle the reply data + if(type == "database") + { + // It's a database file. Ask user where to store the database file. + QString saveFileAs = FileDialog::getSaveFileName(0, qApp->applicationName(), FileDialog::getSqlDatabaseFileFilter(), reply->url().fileName()); + if(!saveFileAs.isEmpty()) + { + // Save the downloaded data under the selected file name + QFile file(saveFileAs); + file.open(QIODevice::WriteOnly); + file.write(reply->readAll()); + file.close(); + + // Tell the application to open this file + emit openFile(saveFileAs); + } + } else if(type == "dir") { + emit gotDirList(reply->readAll(), reply->property("userdata")); } // Delete reply later, i.e. after returning from this slot function m_currentReply = nullptr; - m_progress->hide(); + if(type == "database" || type == "push") + m_progress->hide(); reply->deleteLater(); } @@ -133,7 +143,7 @@ void RemoteDatabase::gotError(QNetworkReply* reply, const QList& erro } // Build an error message and short it to the user - QString message = tr("Error opening remote database file from %1.\n%2").arg(reply->url().toString()).arg(errors.at(0).errorString()); + QString message = tr("Error opening remote file at %1.\n%2").arg(reply->url().toString()).arg(errors.at(0).errorString()); QMessageBox::warning(0, qApp->applicationName(), message); // Delete reply later, i.e. after returning from this slot function @@ -235,7 +245,7 @@ void RemoteDatabase::prepareProgressDialog(bool upload, const QString& url) connect(m_currentReply, &QNetworkReply::downloadProgress, this, &RemoteDatabase::updateProgress); } -void RemoteDatabase::fetchDatabase(const QString& url, const QString& clientCert) +void RemoteDatabase::fetch(const QString& url, bool isDatabase, const QString& clientCert, QVariant userdata) { // Check if network is accessible. If not, abort right here if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible) @@ -260,12 +270,19 @@ void RemoteDatabase::fetchDatabase(const QString& url, const QString& clientCert // Fetch database and save pending reply. Note that we're only supporting one active download here at the moment. m_currentReply = m_manager->get(request); + if(isDatabase) + m_currentReply->setProperty("type", "database"); + else + m_currentReply->setProperty("type", "dir"); + m_currentReply->setProperty("userdata", userdata); - // Initialise the progress dialog for this request - prepareProgressDialog(false, url); + // Initialise the progress dialog for this request, but only if this is a database file. Directory listing are small enough to be loaded + // without progress dialog. + if(isDatabase) + prepareProgressDialog(false, url); } -void RemoteDatabase::pushDatabase(const QString& filename, const QString& url, const QString& clientCert) +void RemoteDatabase::push(const QString& filename, const QString& url, const QString& clientCert) { // Check if network is accessible. If not, abort right here if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible) @@ -304,6 +321,7 @@ void RemoteDatabase::pushDatabase(const QString& filename, const QString& url, c // Fetch database and save pending reply. Note that we're only supporting one active download here at the moment. m_currentReply = m_manager->put(request, file_data); + m_currentReply->setProperty("type", "push"); // Initialise the progress dialog for this request prepareProgressDialog(true, url); diff --git a/src/RemoteDatabase.h b/src/RemoteDatabase.h index f39248bf..7d002992 100644 --- a/src/RemoteDatabase.h +++ b/src/RemoteDatabase.h @@ -24,10 +24,11 @@ public: const QList& caCertificates() const; const QMap& clientCertificates() const { return m_clientCertFiles; } - void fetchDatabase(const QString& url, const QString& clientCert); - void pushDatabase(const QString& filename, const QString& url, const QString& clientCert); + void fetch(const QString& url, bool isDatabase, const QString& clientCert, QVariant userdata = QVariant()); + void push(const QString& filename, const QString& url, const QString& clientCert); signals: + void gotDirList(QString json, QVariant userdata); void openFile(QString path); private: diff --git a/src/RemoteDock.cpp b/src/RemoteDock.cpp new file mode 100644 index 00000000..8a1db1e8 --- /dev/null +++ b/src/RemoteDock.cpp @@ -0,0 +1,74 @@ +#include + +#include "RemoteDock.h" +#include "ui_RemoteDock.h" +#include "Settings.h" +#include "RemoteDatabase.h" +#include "RemoteModel.h" +#include "MainWindow.h" + +RemoteDock::RemoteDock(MainWindow* parent) + : QDialog(parent), + ui(new Ui::RemoteDock), + remoteDatabase(parent->getRemote()), + remoteModel(new RemoteModel(this, parent->getRemote())) +{ + ui->setupUi(this); + + // Set up model + ui->treeStructure->setModel(remoteModel); + + // Initial setup + reloadSettings(); +} + +RemoteDock::~RemoteDock() +{ + delete ui; +} + +void RemoteDock::reloadSettings() +{ + // Load list of client certs + ui->comboUser->clear(); + QStringList client_certs = Settings::getSettingsValue("remote", "client_certificates").toStringList(); + foreach(const QString& file, client_certs) + { + auto certs = QSslCertificate::fromPath(file); + foreach(const QSslCertificate& cert, certs) + ui->comboUser->addItem(cert.subjectInfo(QSslCertificate::CommonName).at(0), file); + } +} + +void RemoteDock::setNewIdentity() +{ + // Get identity + QString identity = ui->comboUser->currentText(); + if(identity.isEmpty()) + return; + + // Get certificate file name + QString cert = ui->comboUser->itemData(ui->comboUser->findText(identity), Qt::UserRole).toString(); + if(cert.isEmpty()) + return; + + // Open root directory. Get host name from client cert + QString cn = remoteDatabase.clientCertificates()[cert].subjectInfo(QSslCertificate::CommonName).at(0); + QStringList cn_parts = cn.split("@"); + if(cn_parts.size() < 2) + return; + remoteModel->setNewRootDir(QString("https://%1:5550/").arg(cn_parts.last()), cert); +} + +void RemoteDock::fetchDatabase(const QModelIndex& idx) +{ + if(!idx.isValid()) + return; + + // Get item + const RemoteModelItem* item = remoteModel->modelIndexToItem(idx); + + // Only open database file + if(item->value(RemoteModelColumnType).toString() == "database") + remoteDatabase.fetch(item->value(RemoteModelColumnUrl).toString(), true, remoteModel->currentClientCertificate()); +} diff --git a/src/RemoteDock.h b/src/RemoteDock.h new file mode 100644 index 00000000..1f6d8bfa --- /dev/null +++ b/src/RemoteDock.h @@ -0,0 +1,35 @@ +#ifndef REMOTEDOCK_H +#define REMOTEDOCK_H + +#include + +class RemoteDatabase; +class RemoteModel; +class MainWindow; + +namespace Ui { +class RemoteDock; +} + +class RemoteDock : public QDialog +{ + Q_OBJECT + +public: + explicit RemoteDock(MainWindow* parent); + ~RemoteDock(); + + void reloadSettings(); + +private slots: + void setNewIdentity(); + void fetchDatabase(const QModelIndex& idx); + +private: + Ui::RemoteDock* ui; + + RemoteDatabase& remoteDatabase; + RemoteModel* remoteModel; +}; + +#endif diff --git a/src/RemoteDock.ui b/src/RemoteDock.ui new file mode 100644 index 00000000..91536a59 --- /dev/null +++ b/src/RemoteDock.ui @@ -0,0 +1,132 @@ + + + RemoteDock + + + + 0 + 0 + 575 + 310 + + + + Remote + + + + + + + + B&rowse + + + comboBrowseMode + + + + + + + + Remote + + + + + Local + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Identity + + + comboUser + + + + + + + QComboBox::AdjustToContents + + + + + + + Go + + + + :/icons/cog_go.png:/icons/cog_go.png + + + + + + + + + + + + + + + + buttonLogin + clicked() + RemoteDock + setNewIdentity() + + + 551 + 22 + + + 419 + 24 + + + + + treeStructure + doubleClicked(QModelIndex) + RemoteDock + fetchDatabase(QModelIndex) + + + 211 + 75 + + + 204 + 37 + + + + + + setNewIdentity() + fetchDatabase(QModelIndex) + + diff --git a/src/RemoteModel.cpp b/src/RemoteModel.cpp new file mode 100644 index 00000000..e0dbe06a --- /dev/null +++ b/src/RemoteModel.cpp @@ -0,0 +1,315 @@ +#include +#include +#include +#include + +#include "RemoteModel.h" +#include "RemoteDatabase.h" + +RemoteModelItem::RemoteModelItem(RemoteModelItem* parent) : + m_parent(parent), + m_fetchedDirectoryList(false) +{ +} + +RemoteModelItem::~RemoteModelItem() +{ + qDeleteAll(m_children); +} + +QVariant RemoteModelItem::value(RemoteModelColumns column) const +{ + return m_values[column]; +} + +void RemoteModelItem::setValue(RemoteModelColumns column, QVariant value) +{ + m_values[column] = value; +} + +void RemoteModelItem::appendChild(RemoteModelItem *item) +{ + m_children.append(item); +} + +RemoteModelItem* RemoteModelItem::child(int row) const +{ + return m_children.value(row); +} + +RemoteModelItem* RemoteModelItem::parent() const +{ + return m_parent; +} + +int RemoteModelItem::childCount() const +{ + return m_children.count(); +} + +int RemoteModelItem::row() const +{ + if(m_parent) + return m_parent->m_children.indexOf(const_cast(this)); + + return 0; +} + +bool RemoteModelItem::fetchedDirectoryList() const +{ + return m_fetchedDirectoryList; +} + +void RemoteModelItem::setFetchedDirectoryList(bool fetched) +{ + m_fetchedDirectoryList = fetched; +} + +QList RemoteModelItem::loadArray(const QJsonValue& value, RemoteModelItem* parent) +{ + QList items; + + // Loop through all directory items + QJsonArray array = value.toArray(); + for(int i=0;isetValue(RemoteModelColumnName, array.at(i).toObject().value("name")); + item->setValue(RemoteModelColumnType, array.at(i).toObject().value("type")); + item->setValue(RemoteModelColumnUrl, array.at(i).toObject().value("url")); + item->setValue(RemoteModelColumnVersion, array.at(i).toObject().value("version")); + item->setValue(RemoteModelColumnSize, array.at(i).toObject().value("size")); + item->setValue(RemoteModelColumnLastModified, array.at(i).toObject().value("last_modified")); + + items.push_back(item); + } + + return items; +} + +RemoteModel::RemoteModel(QObject* parent, RemoteDatabase& remote) : + QAbstractItemModel(parent), + rootItem(new RemoteModelItem()), + remoteDatabase(remote) +{ + // Initialise list of column names + headerList << tr("Name") << tr("Version") << tr("Last modified") << tr("Size"); + + // Set up signals + connect(&remoteDatabase, &RemoteDatabase::gotDirList, this, &RemoteModel::parseDirectoryListing); +} + +RemoteModel::~RemoteModel() +{ + delete rootItem; +} + +void RemoteModel::setNewRootDir(const QString& url, const QString& cert) +{ + // Save settings + currentRootDirectory = url; + currentClientCert = cert; + + // Fetch root directory and put the reply data under the root item + remoteDatabase.fetch(currentRootDirectory, false, currentClientCert, QModelIndex()); +} + +void RemoteModel::parseDirectoryListing(const QString& json, const QVariant& userdata) +{ + // Load new JSON root document assuming it's an array + QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8()); + if(doc.isNull() || !doc.isArray()) + return; + QJsonArray array = doc.array(); + + // Get model index to store the new data under + QModelIndex parent = userdata.toModelIndex(); + RemoteModelItem* parentItem = const_cast(modelIndexToItem(parent)); + + // An invalid model index indicates that this is a new root item. This means the old one needs to be entirely deleted first. + if(!parent.isValid()) + { + // Clear root item + beginResetModel(); + delete rootItem; + rootItem = new RemoteModelItem(); + endResetModel(); + + // Set parent model index and parent item to the new values + parent = QModelIndex(); + parentItem = rootItem; + } + + // Insert data + beginInsertRows(parent, 0, array.size()); + QList items = RemoteModelItem::loadArray(QJsonValue(array), parentItem); + foreach(RemoteModelItem* item, items) + parentItem->appendChild(item); + endInsertRows(); +} + +QModelIndex RemoteModel::index(int row, int column, const QModelIndex& parent) const +{ + if(!hasIndex(row, column, parent)) + return QModelIndex(); + + const RemoteModelItem* parentItem = modelIndexToItem(parent); + RemoteModelItem* childItem = parentItem->child(row); + + if(childItem) + return createIndex(row, column, childItem); + else + return QModelIndex(); +} + +QModelIndex RemoteModel::parent(const QModelIndex& index) const +{ + if(!index.isValid()) + return QModelIndex(); + + const RemoteModelItem* childItem = modelIndexToItem(index); + RemoteModelItem* parentItem = childItem->parent(); + + if(parentItem == rootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +QVariant RemoteModel::data(const QModelIndex& index, int role) const +{ + // Don't return data for invalid indices + if(!index.isValid()) + return QVariant(); + + // Type of item + const RemoteModelItem* item = modelIndexToItem(index); + QString type = item->value(RemoteModelColumnType).toString(); + + // Decoration role? Only for first column! + if(role == Qt::DecorationRole && index.column() == 0) + { + // Use different icons depending on item type + if(type == "folder") + return QImage(":/icons/folder"); + else if(type == "database") + return QImage(":/icons/database"); + } else if(role == Qt::DisplayRole) { + // Display role? + + // Return different value depending on column + switch(index.column()) + { + case 0: + { + return item->value(RemoteModelColumnName); + } + case 1: + { + if(type == "folder") + return QVariant(); + return QString::number(item->value(RemoteModelColumnVersion).toInt()); + } + case 2: + { + return item->value(RemoteModelColumnLastModified); + } + case 3: + { + // Folders don't have a size + if(type == "folder") + return QVariant(); + + // Convert size to human readable format + float size = item->value(RemoteModelColumnSize).toInt(); + QStringList list; + list << "KiB" << "MiB" << "GiB" << "TiB"; + QStringListIterator it(list); + QString unit(tr("bytes")); + while(size >= 1024.0f && it.hasNext()) + { + unit = it.next(); + size /= 1024.0; + } + return QString().setNum(size, 'f', 2) + " " + unit; + } + } + } + + return QVariant(); +} + +QVariant RemoteModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + // Call default implementation for vertical headers and for non-display roles + if(role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QAbstractItemModel::headerData(section, orientation, role); + + // Return header string depending on column + return headerList.at(section); +} + +int RemoteModel::rowCount(const QModelIndex& parent) const +{ + if(parent.column() > 0) + return 0; + + const RemoteModelItem* parentItem = modelIndexToItem(parent); + return parentItem->childCount(); +} + +int RemoteModel::columnCount(const QModelIndex& /*parent*/) const +{ + return headerList.size(); +} + +bool RemoteModel::hasChildren(const QModelIndex& parent) const +{ + if(!parent.isValid()) + return true; + + // If the item actually has children or is of type "folder" (and may have no children yet), we say that it actually has children + const RemoteModelItem* item = modelIndexToItem(parent); + return item->childCount() || item->value(RemoteModelColumnType) == "folder"; +} + +bool RemoteModel::canFetchMore(const QModelIndex& parent) const +{ + if(!parent.isValid()) + return false; + + // If the item is of type "folder" and we haven't tried fetching a directory listing yet, we indicate that there might be more data to load + const RemoteModelItem* item = modelIndexToItem(parent); + return item->value(RemoteModelColumnType) == "folder" && !item->fetchedDirectoryList(); +} + +void RemoteModel::fetchMore(const QModelIndex& parent) +{ + // Can we even fetch more data? + if(!canFetchMore(parent)) + return; + + // Get parent item + RemoteModelItem* item = static_cast(parent.internalPointer()); + + // Fetch item URL + item->setFetchedDirectoryList(true); + remoteDatabase.fetch(item->value(RemoteModelColumnUrl).toString(), false, currentClientCert, parent); +} + +const QString& RemoteModel::currentClientCertificate() const +{ + return currentClientCert; +} + +const RemoteModelItem* RemoteModel::modelIndexToItem(const QModelIndex& idx) const +{ + if(!idx.isValid()) + return rootItem; + else + return static_cast(idx.internalPointer()); +} diff --git a/src/RemoteModel.h b/src/RemoteModel.h new file mode 100644 index 00000000..be050157 --- /dev/null +++ b/src/RemoteModel.h @@ -0,0 +1,110 @@ +#ifndef REMOTEMODEL_H +#define REMOTEMODEL_H + +#include +#include + +class RemoteDatabase; + +// List of fields stored in the JSON data +enum RemoteModelColumns +{ + RemoteModelColumnName, + RemoteModelColumnType, + RemoteModelColumnUrl, + RemoteModelColumnVersion, + RemoteModelColumnSize, + RemoteModelColumnLastModified, + + RemoteModelColumnCount +}; + +class RemoteModelItem +{ +public: + RemoteModelItem(RemoteModelItem* parent = nullptr); + ~RemoteModelItem(); + + QVariant value(RemoteModelColumns column) const; + void setValue(RemoteModelColumns column, QVariant value); + + bool fetchedDirectoryList() const; + void setFetchedDirectoryList(bool fetched); + + void appendChild(RemoteModelItem* item); + RemoteModelItem* child(int row) const; + RemoteModelItem* parent() const; + int childCount() const; + int row() const; + + // This function assumes the JSON value it's getting passed is an array ("[{...}, {...}, {...}, ...]"). It returns a list of model items, one + // per array entry and each with the specified parent set. + static QList loadArray(const QJsonValue& value, RemoteModelItem* parent = nullptr); + +private: + // These are just the fields from the json objects returned by the dbhub.io server + QVariant m_values[RemoteModelColumnCount]; + + // Child items and parent item + QList m_children; + RemoteModelItem* m_parent; + + // Indicates whether we already tried fetching a directory listing for this item. This serves two purposes: + // 1) When having an empty directory this allows us to remove the expandable flag for this item. + // 2) Between sending a network request and getting the reply this flag is already set, avoiding a second or third request being sent in the meantime. + bool m_fetchedDirectoryList; +}; + +class RemoteModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit RemoteModel(QObject* parent, RemoteDatabase& remote); + virtual ~RemoteModel(); + + void setNewRootDir(const QString& url, const QString& cert); + + QModelIndex index(int row, int column,const QModelIndex& parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + bool hasChildren(const QModelIndex& parent) const; + + bool canFetchMore(const QModelIndex& parent) const; + void fetchMore(const QModelIndex& parent); + + // This helper function takes a model index and returns the according model item. An invalid model index is used to indicate the + // root item, so if the index is invalid the root item is returned. This means that if you need to check for actual invalid indices + // this needs to be done prior to calling this function. + const RemoteModelItem* modelIndexToItem(const QModelIndex& idx) const; + + // Returns the current client certificate + const QString& currentClientCertificate() const; + +private slots: + // This is called whenever a network reply containing a directory listing arrives. json contains the reply data, userdata + // contains some custom data passed to the request. In this case we expect this to be the model index of the parent tree item. + void parseDirectoryListing(const QString& json, const QVariant& userdata); + +private: + // Pointer to the root item. This contains all the actual item data. + RemoteModelItem* rootItem; + + // Thr header list is a list of column titles. It's a static list that's getting filled in the constructor. + QStringList headerList; + + // Reference to the remote database object which is stored somewhere in the main window. + RemoteDatabase& remoteDatabase; + + // This stores the currently used network identity so it can be used for further requests, e.g. for + // lazy population. + QString currentRootDirectory; + QString currentClientCert; +}; + +#endif diff --git a/src/icons/cog_go.png b/src/icons/cog_go.png new file mode 100644 index 00000000..3262767c Binary files /dev/null and b/src/icons/cog_go.png differ diff --git a/src/icons/database.png b/src/icons/database.png new file mode 100644 index 00000000..3d09261a Binary files /dev/null and b/src/icons/database.png differ diff --git a/src/icons/folder.png b/src/icons/folder.png new file mode 100644 index 00000000..784e8fa4 Binary files /dev/null and b/src/icons/folder.png differ diff --git a/src/icons/icons.qrc b/src/icons/icons.qrc index 4396135b..2fe96579 100644 --- a/src/icons/icons.qrc +++ b/src/icons/icons.qrc @@ -51,5 +51,8 @@ picture_edit.png script_edit.png tag_blue_edit.png + folder.png + database.png + cog_go.png diff --git a/src/src.pro b/src/src.pro index 404d77cd..dec13b7f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -54,7 +54,9 @@ HEADERS += \ FilterLineEdit.h \ RemoteDatabase.h \ ForeignKeyEditorDelegate.h \ - PlotDock.h + PlotDock.h \ + RemoteDock.h \ + RemoteModel.h SOURCES += \ sqlitedb.cpp \ @@ -87,7 +89,9 @@ SOURCES += \ FilterLineEdit.cpp \ RemoteDatabase.cpp \ ForeignKeyEditorDelegate.cpp \ - PlotDock.cpp + PlotDock.cpp \ + RemoteDock.cpp \ + RemoteModel.cpp RESOURCES += icons/icons.qrc \ translations/flags/flags.qrc \ @@ -108,7 +112,8 @@ FORMS += \ CipherDialog.ui \ ExportSqlDialog.ui \ ColumnDisplayFormatDialog.ui \ - PlotDock.ui + PlotDock.ui \ + RemoteDock.ui TRANSLATIONS += \ translations/sqlb_ar_SA.ts \