From 3e4f3fc3d20ad68e4811e2f09d067cd1cb2ec1c0 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sat, 18 Mar 2017 20:40:59 +0100 Subject: [PATCH] dbhub: Add remote dock with directory browsing support This adds a new dock to the main window that contains all the remote functionality (or is supposed to contain it all in the future). It also adds a directory browsing feature which allows you to browse through the folders and files on the dbhub server. By double clicking a database you can download and open it. The Open Remote menu action isn't needed anymore and has been removed. This also fixes an issue with pushing databases where, after sending the file is completed, the save dialog was opened. Note that this is still WIP and is far from polished. --- CMakeLists.txt | 5 + src/MainWindow.cpp | 39 ++--- src/MainWindow.h | 9 +- src/MainWindow.ui | 15 +- src/RemoteDatabase.cpp | 54 ++++--- src/RemoteDatabase.h | 5 +- src/RemoteDock.cpp | 74 ++++++++++ src/RemoteDock.h | 35 +++++ src/RemoteDock.ui | 132 +++++++++++++++++ src/RemoteModel.cpp | 315 +++++++++++++++++++++++++++++++++++++++++ src/RemoteModel.h | 110 ++++++++++++++ src/icons/cog_go.png | Bin 0 -> 859 bytes src/icons/database.png | Bin 0 -> 390 bytes src/icons/folder.png | Bin 0 -> 537 bytes src/icons/icons.qrc | 3 + src/src.pro | 11 +- 16 files changed, 758 insertions(+), 49 deletions(-) create mode 100644 src/RemoteDock.cpp create mode 100644 src/RemoteDock.h create mode 100644 src/RemoteDock.ui create mode 100644 src/RemoteModel.cpp create mode 100644 src/RemoteModel.h create mode 100644 src/icons/cog_go.png create mode 100644 src/icons/database.png create mode 100644 src/icons/folder.png 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 0000000000000000000000000000000000000000..3262767cda95a217ac8d3fabf6c4bfe3514fd770 GIT binary patch literal 859 zcmV-h1El-)HU~Z@BQ7|ITsWqFPt%czwZJk^u%JZL0Ogu z7-JwwQn0tTclx3}?kvID+L{XidsxQ^&e&|W7P?hu`JbU6)Keq zD2hUaNxZkB72a$%4^2)^`UtC|A7te-nGAX1Xrjep5qO>_@7ffX%SGn`-ED7gLpU5( zk(@u5K{Og2Z)$29rKuELp-_NyIt_Job>MI~K&R7bgynLX>`sh~jA#lt3}_YQwqglZ ztJPR4mEiF3kO&v?|I7ONdO%xaZnyg`4MVH2u&{85WF^F8xkx0!oK7d7&*!07ENT-s zHZ~xYO7;1CzImGB_xm6GXq~MiVV0C|gzZGQ)-QC?rN*&h*fy7fxUu2>pgCsM)-Q?tMb$Vds z*E@*sEW?$R-d!Zlo`yI#H#gqa);3B?D6pBWXVBO67?`R6Qy3_qLZ+|_MxhlJx8`9r z{Xs@mdTouNQ0N7+J#TJqhNGh+O#w+J@OC~45~`3D2_z=L-&zrFU%dy%Qdzg0ic~cM z%s{~na19L&^isj*=4PpCtqL-e!E)J#V5X7%DWt(#}Pq3W$oGSy^Pc4j%jrk7_ z4xV5*S(C}slXQfB*Dx$m5ut)=uA6Vdoof#vmX1Pr{o_H2ueAT3P;2Ktrs3gX7h2gv zE62G1jK||?p|odbXLH|f%y4eoeRKHd`|tRw^&nXM?`u5!c)i}iTrM|2E9N*Z__gcJ lE2dmBR}@y4olxbIzJJd{=EC=vF*^VN002ovPDHLkV1lGDi4On( literal 0 HcmV?d00001 diff --git a/src/icons/database.png b/src/icons/database.png new file mode 100644 index 0000000000000000000000000000000000000000..3d09261a26eb97c6dedc1d3504cbc2cf915eb642 GIT binary patch literal 390 zcmV;10eSw3P);1k*-!zk~CMF9Bv_3(^PCOq;x(K@^6+>g^d@v4;gkbWsEoXE%32*i1tcpTNXd5CcIl)ECgqz|2rE6EW}s7R?kl za1q`0GCkMruC6-2LANtwVlsgzsp4?{@7$`KBv!G66>Vie3h?3OmEEkjwdLG0PgLVi z`!N((f$A@n17Ldj#`};0I3@iHJ5M{#IZz|UIYRm4(!uV7eYIYIwQf&}_2J~}>pQ^n z6o8--^T(=hkBNQ_k{-_GWE;FMW7!p}f{NG3nHZ{D5<3d8&tLh%a4AqqnjMkr3m&fkMdECD3N5}Unig5wy40;>lo4j~k+e}v)` zR6)J8Mk*u=SpB`p6o)7j?S0T@9?bz#m@l>gc*zk__|*!FMcHwP!gwLJvS~9c0px8E zWpicture_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 \