From 9c03eb69bcb6913c78eed7b4e6f9184aa6ed2ee9 Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 8 Feb 2017 13:18:58 +0000 Subject: [PATCH] Add remote file browsing dialog --- qrenderdoc/Code/QRDUtils.cpp | 10 +- qrenderdoc/Code/QRDUtils.h | 6 +- qrenderdoc/Code/RenderManager.cpp | 36 +- qrenderdoc/Code/RenderManager.h | 4 +- qrenderdoc/Windows/Dialogs/CaptureDialog.cpp | 61 +- .../Windows/Dialogs/VirtualFileDialog.cpp | 826 ++++++++++++++++++ .../Windows/Dialogs/VirtualFileDialog.h | 83 ++ .../Windows/Dialogs/VirtualFileDialog.ui | 272 ++++++ qrenderdoc/qrenderdoc.pro | 9 +- qrenderdoc/qrenderdoc_local.vcxproj | 5 + qrenderdoc/qrenderdoc_local.vcxproj.filters | 15 + 11 files changed, 1280 insertions(+), 47 deletions(-) create mode 100644 qrenderdoc/Windows/Dialogs/VirtualFileDialog.cpp create mode 100644 qrenderdoc/Windows/Dialogs/VirtualFileDialog.h create mode 100644 qrenderdoc/Windows/Dialogs/VirtualFileDialog.ui diff --git a/qrenderdoc/Code/QRDUtils.cpp b/qrenderdoc/Code/QRDUtils.cpp index d061c75c2..1a846c58f 100644 --- a/qrenderdoc/Code/QRDUtils.cpp +++ b/qrenderdoc/Code/QRDUtils.cpp @@ -378,14 +378,12 @@ QMessageBox::StandardButton RDDialog::messageBox(QMessageBox::Icon icon, QWidget } QString RDDialog::getExistingDirectory(QWidget *parent, const QString &caption, const QString &dir, - QFileDialog::Options options, QAbstractProxyModel *proxy) + QFileDialog::Options options) { QFileDialog fd(parent, caption, dir, QString()); fd.setAcceptMode(QFileDialog::AcceptOpen); fd.setFileMode(QFileDialog::DirectoryOnly); fd.setOptions(options); - if(proxy) - fd.setProxyModel(proxy); show(&fd); if(fd.result() == QFileDialog::Accepted) @@ -422,7 +420,7 @@ QString RDDialog::getOpenFileName(QWidget *parent, const QString &caption, const } QString RDDialog::getExecutableFileName(QWidget *parent, const QString &caption, const QString &dir, - QFileDialog::Options options, QAbstractProxyModel *proxy) + QFileDialog::Options options) { QString filter; @@ -435,13 +433,11 @@ QString RDDialog::getExecutableFileName(QWidget *parent, const QString &caption, fd.setOptions(options); fd.setAcceptMode(QFileDialog::AcceptOpen); fd.setFileMode(QFileDialog::ExistingFile); - if(!proxy) { QFileFilterModel *fileProxy = new QFileFilterModel(parent); fileProxy->setRequirePermissions(QDir::Executable); - proxy = fileProxy; + fd.setProxyModel(fileProxy); } - fd.setProxyModel(proxy); show(&fd); if(fd.result() == QFileDialog::Accepted) diff --git a/qrenderdoc/Code/QRDUtils.h b/qrenderdoc/Code/QRDUtils.h index 58d668283..b5603f434 100644 --- a/qrenderdoc/Code/QRDUtils.h +++ b/qrenderdoc/Code/QRDUtils.h @@ -628,8 +628,7 @@ struct RDDialog static QString getExistingDirectory(QWidget *parent = NULL, const QString &caption = QString(), const QString &dir = QString(), - QFileDialog::Options options = QFileDialog::ShowDirsOnly, - QAbstractProxyModel *proxy = NULL); + QFileDialog::Options options = QFileDialog::ShowDirsOnly); static QString getOpenFileName(QWidget *parent = NULL, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), @@ -638,8 +637,7 @@ struct RDDialog static QString getExecutableFileName(QWidget *parent = NULL, const QString &caption = QString(), const QString &dir = QString(), - QFileDialog::Options options = QFileDialog::Options(), - QAbstractProxyModel *proxy = NULL); + QFileDialog::Options options = QFileDialog::Options()); static QString getSaveFileName(QWidget *parent = NULL, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), diff --git a/qrenderdoc/Code/RenderManager.cpp b/qrenderdoc/Code/RenderManager.cpp index cef5a8bbd..4da3e47c5 100644 --- a/qrenderdoc/Code/RenderManager.cpp +++ b/qrenderdoc/Code/RenderManager.cpp @@ -100,16 +100,21 @@ QStringList RenderManager::GetRemoteSupport() return ret; } -void RenderManager::GetHomeFolder(DirectoryBrowseMethod cb) +void RenderManager::GetHomeFolder(bool synchronous, DirectoryBrowseMethod cb) { if(!m_Remote) return; if(IsRunning() && m_Thread->isCurrentThread()) { - AsyncInvoke([cb, this](IReplayRenderer *r) { + auto lambda = [cb, this](IReplayRenderer *r) { cb(m_Remote->GetHomeFolder().c_str(), rdctype::array()); - }); + }; + + if(synchronous) + BlockInvoke(lambda); + else + AsyncInvoke(lambda); return; } @@ -123,31 +128,35 @@ void RenderManager::GetHomeFolder(DirectoryBrowseMethod cb) cb(home.c_str(), rdctype::array()); } -bool RenderManager::ListFolder(QString path, DirectoryBrowseMethod cb) +bool RenderManager::ListFolder(QString path, bool synchronous, DirectoryBrowseMethod cb) { if(!m_Remote) return false; + QByteArray pathUTF8 = path.toUtf8(); + if(IsRunning() && m_Thread->isCurrentThread()) { - AsyncInvoke([cb, path, this](IReplayRenderer *r) { - const char *pathstr = path.toUtf8().data(); - cb(pathstr, m_Remote->ListFolder(pathstr)); - }); + auto lambda = [cb, pathUTF8, this](IReplayRenderer *r) { + cb(pathUTF8.data(), m_Remote->ListFolder(pathUTF8.data())); + }; + + if(synchronous) + BlockInvoke(lambda); + else + AsyncInvoke(lambda); return true; } rdctype::array contents; - const char *pathstr = path.toUtf8().data(); - // prevent pings while fetching remote FS data { QMutexLocker autolock(&m_RemoteLock); - contents = m_Remote->ListFolder(pathstr); + contents = m_Remote->ListFolder(pathUTF8.data()); } - cb(pathstr, contents); + cb(pathUTF8.data(), contents); return true; } @@ -281,6 +290,9 @@ ReplayCreateStatus RenderManager::ConnectToRemoteServer(RemoteHost *host) void RenderManager::DisconnectFromRemoteServer() { + if(m_RemoteHost) + m_RemoteHost->Connected = false; + if(m_Remote) { QMutexLocker autolock(&m_RemoteLock); diff --git a/qrenderdoc/Code/RenderManager.h b/qrenderdoc/Code/RenderManager.h index f1b1ec2f9..613ab558d 100644 --- a/qrenderdoc/Code/RenderManager.h +++ b/qrenderdoc/Code/RenderManager.h @@ -147,8 +147,8 @@ public: CaptureOptions opts); QStringList GetRemoteSupport(); - void GetHomeFolder(DirectoryBrowseMethod cb); - bool ListFolder(QString path, DirectoryBrowseMethod cb); + void GetHomeFolder(bool synchronous, DirectoryBrowseMethod cb); + bool ListFolder(QString path, bool synchronous, DirectoryBrowseMethod cb); QString CopyCaptureToRemote(const QString &localpath, QWidget *window); void CopyCaptureFromRemote(const QString &remotepath, const QString &localpath, QWidget *window); diff --git a/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp b/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp index d57e0d221..0a02f10c9 100644 --- a/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp +++ b/qrenderdoc/Windows/Dialogs/CaptureDialog.cpp @@ -27,6 +27,7 @@ #include #include "Code/QRDUtils.h" #include "Code/qprocessinfo.h" +#include "Windows/Dialogs/VirtualFileDialog.h" #include "FlowLayout.h" #include "LiveCapture.h" #include "ToolWindowManager.h" @@ -237,15 +238,29 @@ void CaptureDialog::on_processFilter_textChanged(const QString &filter) model->setFilterFixedString(filter); } -void CaptureDialog::on_exePath_textChanged(const QString &exe) +void CaptureDialog::on_exePath_textChanged(const QString &text) { + QString exe = text; + + // This is likely due to someone pasting a full path copied using copy path. Removing the quotes + // is safe in any case + if(exe.startsWith(QChar('"')) && exe.endsWith(QChar('"')) && exe.count() > 2) + { + exe = exe.mid(1, exe.count() - 2); + ui->exePath->setText(exe); + return; + } + QFileInfo f(exe); QDir dir = f.dir(); bool valid = dir.makeAbsolute(); if(valid && f.isAbsolute()) { - QString path = QDir::toNativeSeparators(dir.absolutePath()); + QString path = dir.absolutePath(); + + if(!m_Ctx->Renderer()->remote()) + path = QDir::toNativeSeparators(path); // match the path separators from the path if(exe.count(QChar('/')) > exe.count(QChar('\\'))) @@ -291,17 +306,18 @@ void CaptureDialog::on_exePathBrowse_clicked() file = m_Ctx->Config.LastCaptureExe; } - QAbstractProxyModel *proxy = NULL; - QFileDialog::Options options; + QString filename; if(m_Ctx->Renderer()->remote()) { - // proxy = new RemoteFileProxy(m_Ctx->Renderer()); - options = QFileDialog::DontUseNativeDialog; + VirtualFileDialog vfd(m_Ctx, this); + RDDialog::show(&vfd); + filename = vfd.chosenPath(); + } + else + { + filename = RDDialog::getExecutableFileName(this, tr("Choose executable"), initDir); } - - QString filename = - RDDialog::getExecutableFileName(this, tr("Choose executable"), initDir, options, proxy); if(filename != "") setExecutableFilename(filename); @@ -324,17 +340,19 @@ void CaptureDialog::on_workDirBrowse_clicked() initDir = m_Ctx->Config.LastCapturePath; } - QAbstractProxyModel *proxy = NULL; - QFileDialog::Options options = QFileDialog::ShowDirsOnly; + QString dir; if(m_Ctx->Renderer()->remote()) { - // proxy = new RemoteFileProxy(m_Ctx->Renderer()); - options |= QFileDialog::DontUseNativeDialog; + VirtualFileDialog vfd(m_Ctx, this); + vfd.setDirBrowse(); + RDDialog::show(&vfd); + dir = vfd.chosenPath(); + } + else + { + dir = RDDialog::getExistingDirectory(this, "Choose working directory", initDir); } - - QString dir = - RDDialog::getExistingDirectory(this, "Choose working directory", initDir, options, proxy); if(dir != "") ui->workDirPath->setText(dir); @@ -487,11 +505,16 @@ void CaptureDialog::fillProcessList() void CaptureDialog::setExecutableFilename(QString filename) { - filename = QDir::toNativeSeparators(QFileInfo(filename).absoluteFilePath()); + if(!m_Ctx->Renderer()->remote()) + filename = QDir::toNativeSeparators(QFileInfo(filename).absoluteFilePath()); + ui->exePath->setText(filename); - m_Ctx->Config.LastCapturePath = QFileInfo(filename).absolutePath(); - m_Ctx->Config.LastCaptureExe = QFileInfo(filename).completeBaseName(); + if(!m_Ctx->Renderer()->remote()) + { + m_Ctx->Config.LastCapturePath = QFileInfo(filename).absolutePath(); + m_Ctx->Config.LastCaptureExe = QFileInfo(filename).completeBaseName(); + } } void CaptureDialog::loadSettings(QString filename) diff --git a/qrenderdoc/Windows/Dialogs/VirtualFileDialog.cpp b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.cpp new file mode 100644 index 000000000..fd866769b --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.cpp @@ -0,0 +1,826 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2017 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "VirtualFileDialog.h" +#include +#include +#include +#include +#include +#include +#include "Code/CaptureContext.h" +#include "Code/RenderManager.h" +#include "ui_VirtualFileDialog.h" + +class RemoteFileModel : public QAbstractItemModel +{ +public: + enum Roles + { + FileIsDirRole = Qt::UserRole, + FileIsHiddenRole, + FileIsExecutableRole, + FileIsRootRole, + FileIsAccessDeniedRole, + FilePathRole, + FileNameRole, + }; + + RemoteFileModel(RenderManager *r, QObject *parent = NULL) + : Renderer(*r), QAbstractItemModel(parent) + { + makeIconStates(fileIcon, QString::fromUtf8(":/Resources/page_white_database.png")); + makeIconStates(exeIcon, QString::fromUtf8(":/Resources/page_white_code.png")); + makeIconStates(dirIcon, QString::fromUtf8(":/Resources/folder_page.png")); + + Renderer.GetHomeFolder(true, [this](const char *path, const rdctype::array &files) { + QString homeDir = QString::fromUtf8(path); + + if(QChar(path[0]).isLetter() && path[1] == ':') + { + NTPaths = true; + + // NT paths + Renderer.ListFolder( + "/", true, [this, homeDir](const char *path, const rdctype::array &files) { + for(int i = 0; i < files.count; i++) + { + FSNode *node = new FSNode(); + node->parent = NULL; + node->parentIndex = i; + node->file = files[i]; + roots.push_back(node); + + home = indexForPath(homeDir); + } + }); + } + else + { + NTPaths = false; + + FSNode *node = new FSNode(); + node->parent = NULL; + node->parentIndex = 0; + node->file.filename = "/"; + node->file.flags = eFileProp_Directory; + roots.push_back(node); + + home = indexForPath(homeDir); + } + }); + + for(FSNode *node : roots) + populate(node); + } + + ~RemoteFileModel() + { + for(FSNode *n : roots) + delete n; + } + + QModelIndex homeFolder() const { return home; } + QModelIndex indexForPath(const QString &path) const + { + QModelIndex ret = index(0, 0); + + QString normPath = path; + + // locate the drive + if(NTPaths) + { + // normalise to unix directory separators + normPath.replace(QChar('\\'), QChar('/')); + + for(int i = 0; i < roots.count(); i++) + { + if(normPath[0] == roots[i]->file.filename.front()) + { + ret = index(i, 0); + normPath.remove(0, 2); + break; + } + } + } + + // normPath is now of the form /subdir1/subdir2/subdir3/... + // with ret pointing to the root directory (trivial on unix) + + while(!normPath.isEmpty()) + { + if(normPath[0] != '/') + { + qCritical() << "Malformed/unexpected path" << path; + return QModelIndex(); + } + + // ignore multiple /s adjacent + int start = 1; + while(start < normPath.count() && normPath[start] == '/') + start++; + + // if we've hit trailing slashes just stop + if(start >= normPath.count()) + break; + + int nextDirEnd = normPath.indexOf(QChar('/'), start); + + if(nextDirEnd == -1) + nextDirEnd = normPath.count(); + + QString nextDir = normPath.mid(start, nextDirEnd - start); + normPath.remove(0, nextDirEnd); + + FSNode *node = getNode(ret); + populate(node); + + for(int i = 0; i < node->children.count(); i++) + { + if(ToQStr(node->children[i]->file.filename) + .compare(nextDir, NTPaths ? Qt::CaseInsensitive : Qt::CaseSensitive) == 0) + { + ret = index(i, 0, ret); + break; + } + } + + // if we didn't move to a child, stop searching + if(node == getNode(ret)) + { + qCritical() << "Couldn't find child" << nextDir << "at" << ToQStr(node->file.filename) + << "from" << path; + return QModelIndex(); + } + } + + return ret; + } + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override + { + if(row < 0 || column < 0 || row >= rowCount(parent) || column >= columnCount(parent)) + return QModelIndex(); + + FSNode *node = getNode(parent); + + if(node == NULL) + return createIndex(row, column, roots[row]); + + return createIndex(row, column, node->children[row]); + } + + QModelIndex parent(const QModelIndex &index) const override + { + if(!index.isValid()) + return QModelIndex(); + + FSNode *node = getNode(index); + + // root nodes have no index + if(node->parent == NULL) + return QModelIndex(); + + node = node->parent; + + return createIndex(node->parentIndex, 0, node); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + if(!parent.isValid()) + return roots.count(); + + return getNode(parent)->children.count(); + } + + int columnCount(const QModelIndex &parent = QModelIndex()) const override + { + // Name | Date Modified | Type | Size + return 4; + } + + Qt::ItemFlags flags(const QModelIndex &index) const override + { + Qt::ItemFlags ret = QAbstractItemModel::flags(index); + + // disable drag/drop + ret &= ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled); + + // disable editing, we don't support remote renaming + ret &= ~Qt::ItemIsEditable; + + if(!index.isValid()) + return ret; + + // if it's not a dir, there are no children + if(getNode(index)->file.flags & eFileProp_Directory) + ret &= ~Qt::ItemNeverHasChildren; + + // if we can't populate it, set it as disabled + if(getNode(index)->file.flags & eFileProp_ErrorAccessDenied) + ret &= ~Qt::ItemIsEnabled; + + return ret; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch(section) + { + case 0: return tr("Name"); + case 1: return tr("Size"); + case 2: return tr("Type"); + case 3: return tr("Date Modified"); + default: break; + } + } + + return QVariant(); + } + + bool canFetchMore(const QModelIndex &parent) const override + { + FSNode *node = getNode(parent); + if(!node) + return true; + + if(!node->populated) + return true; + + for(FSNode *c : node->children) + if(!c->populated) + return true; + + return false; + } + + void fetchMore(const QModelIndex &parent) override + { + FSNode *node = getNode(parent); + if(!node) + return; + + populate(node); + for(FSNode *c : node->children) + populate(c); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if(index.isValid()) + { + FSNode *node = getNode(index); + + switch(role) + { + case Qt::DisplayRole: + { + switch(index.column()) + { + case 0: { return ToQStr(node->file.filename); + } + case 1: + { + if(node->file.flags & eFileProp_Directory) + return QVariant(); + return qulonglong(node->file.size); + } + case 2: + { + if(node->file.flags & eFileProp_Directory) + return tr("Directory"); + else if(node->file.flags & eFileProp_Executable) + return tr("Executable file"); + else + return tr("File"); + } + case 3: + { + if(node->file.lastmod == 0) + return QVariant(); + return QDateTime::fromTime_t(node->file.lastmod); + } + default: break; + } + break; + } + case Qt::DecorationRole: + if(index.column() == 0) + { + int hideIndex = (node->file.flags & eFileProp_Hidden) ? 1 : 0; + + if(node->file.flags & eFileProp_Directory) + return dirIcon[hideIndex]; + else if(node->file.flags & eFileProp_Executable) + return exeIcon[hideIndex]; + else + return fileIcon[hideIndex]; + } + case Qt::TextAlignmentRole: + if(index.column() == 1) + return Qt::AlignRight; + break; + case FileIsDirRole: return bool(node->file.flags & eFileProp_Directory); + case FileIsHiddenRole: return bool(node->file.flags & eFileProp_Hidden); + case FileIsExecutableRole: return bool(node->file.flags & eFileProp_Executable); + case FileIsRootRole: return roots.contains(node); + case FileIsAccessDeniedRole: return bool(node->file.flags & eFileProp_ErrorAccessDenied); + case FilePathRole: return makePath(node); + case FileNameRole: return ToQStr(node->file.filename); + default: break; + } + } + + return QVariant(); + } + +private: + RenderManager &Renderer; + + QIcon dirIcon[2]; + QIcon exeIcon[2]; + QIcon fileIcon[2]; + + void makeIconStates(QIcon *icon, const QString &iconSource) + { + QPixmap normalPixmap(iconSource); + QPixmap disabledPixmap(normalPixmap.size()); + disabledPixmap.fill(Qt::transparent); + QPainter p(&disabledPixmap); + + p.setBackgroundMode(Qt::TransparentMode); + p.setBackground(QBrush(Qt::transparent)); + p.eraseRect(normalPixmap.rect()); + + p.setOpacity(0.5); + p.drawPixmap(0, 0, normalPixmap); + + p.end(); + + icon[0].addPixmap(normalPixmap); + icon[1].addPixmap(disabledPixmap); + } + + struct FSNode + { + FSNode() { memset(&file, 0, sizeof(file)); } + ~FSNode() + { + for(FSNode *n : children) + delete n; + } + + FSNode *parent = NULL; + int parentIndex = 0; + + bool populated = false; + + DirectoryFile file; + + QList children; + }; + + bool NTPaths = false; + QList roots; + QModelIndex home; + + FSNode *getNode(const QModelIndex &idx) const + { + FSNode *node = (FSNode *)idx.internalPointer(); + return node; + } + + QString makePath(FSNode *node) const + { + QChar sep = NTPaths ? '\\' : '/'; + QString ret = ToQStr(node->file.filename); + FSNode *parent = node->parent; + // iterate through subdirs but stop before a root + while(parent && parent->parent) + { + ret = ToQStr(parent->file.filename) + sep + ret; + parent = parent->parent; + } + + if(parent) + { + // parent is now a root + ret = ToQStr(parent->file.filename) + ret; + } + ret.replace('/', sep); + return ret; + } + + void populate(FSNode *node) const + { + if(!node || node->populated) + return; + + node->populated = true; + + // nothing to do for non-directories + if(!(node->file.flags & eFileProp_Directory)) + return; + + Renderer.ListFolder( + makePath(node), true, + [this, node](const char *path, const rdctype::array &files) { + + if(files.count == 1 && files[0].flags & eFileProp_ErrorAccessDenied) + { + node->file.flags |= eFileProp_ErrorAccessDenied; + return; + } + + QVector sortedFiles; + sortedFiles.reserve(files.count); + for(const DirectoryFile &f : files) + sortedFiles.push_back(f); + + qSort(sortedFiles.begin(), sortedFiles.end(), + [](const DirectoryFile &a, const DirectoryFile &b) { + // sort greater than so that files with the flag are sorted before those without + if((a.flags & eFileProp_Directory) != (b.flags & eFileProp_Directory)) + return (a.flags & eFileProp_Directory) > (b.flags & eFileProp_Directory); + + return strcmp(a.filename.c_str(), b.filename.c_str()) < 0; + }); + + for(int i = 0; i < sortedFiles.count(); i++) + { + FSNode *child = new FSNode(); + child->parent = node; + child->parentIndex = i; + child->file = sortedFiles[i]; + node->children.push_back(child); + } + }); + } +}; + +class RemoteFileProxy : public QSortFilterProxyModel +{ +public: + RemoteFileProxy(QObject *parent = NULL) : QSortFilterProxyModel(parent) {} + int columnCount(const QModelIndex &parent = QModelIndex()) const override + { + return qMin(maxColCount, sourceModel()->columnCount(parent)); + } + + void refresh() { QSortFilterProxyModel::filterChanged(); } + int maxColCount = INT_MAX; + bool showFiles = true; + bool showDirs = true; + bool showHidden = true; + bool showNonExecutables = true; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override + { + bool isDir = sourceModel() + ->data(sourceModel()->index(source_row, 0, source_parent), + RemoteFileModel::FileIsDirRole) + .toBool(); + + if(!showDirs && isDir) + return false; + + if(!showFiles && !isDir) + return false; + + bool isHidden = sourceModel() + ->data(sourceModel()->index(source_row, 0, source_parent), + RemoteFileModel::FileIsHiddenRole) + .toBool(); + + if(!showHidden && isHidden) + return false; + + // if we're showing dirs, never apply further filters like filename matching + if(isDir) + return true; + + bool isExe = sourceModel() + ->data(sourceModel()->index(source_row, 0, source_parent), + RemoteFileModel::FileIsExecutableRole) + .toBool(); + + if(!showNonExecutables && !isExe) + return false; + + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + } + + virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override + { + // always sort dirs first + bool isLeftDir = sourceModel()->data(source_left, RemoteFileModel::FileIsDirRole).toBool(); + bool isRightDir = sourceModel()->data(source_right, RemoteFileModel::FileIsDirRole).toBool(); + + if(isLeftDir && !isRightDir) + return true; + if(!isLeftDir && isRightDir) + return false; + + return QSortFilterProxyModel::lessThan(source_left, source_right); + } +}; + +VirtualFileDialog::VirtualFileDialog(CaptureContext *ctx, QWidget *parent) + : QDialog(parent), ui(new Ui::VirtualFileDialog) +{ + ui->setupUi(this); + + m_Model = new RemoteFileModel(ctx->Renderer(), this); + + m_DirProxy = new RemoteFileProxy(this); + m_DirProxy->setSourceModel(m_Model); + + m_DirProxy->showFiles = false; + m_DirProxy->showHidden = ui->showHidden->isChecked(); + m_DirProxy->maxColCount = 1; + + m_FileProxy = new RemoteFileProxy(this); + m_FileProxy->setSourceModel(m_Model); + + m_FileProxy->showHidden = ui->showHidden->isChecked(); + + ui->dirList->setModel(m_DirProxy); + ui->fileList->setModel(m_FileProxy); + + ui->fileList->sortByColumn(0, Qt::AscendingOrder); + + ui->fileList->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->fileList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->fileList->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->fileList->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + + ui->filter->addItems({tr("Executables"), tr("All Files")}); + + ui->back->setEnabled(false); + ui->forward->setEnabled(false); + ui->upFolder->setEnabled(false); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setDefault(false); + + // switch to home folder and expand it + changeCurrentDir(m_Model->homeFolder()); + ui->dirList->expand(m_DirProxy->mapFromSource(currentDir())); +} + +VirtualFileDialog::~VirtualFileDialog() +{ + delete ui; +} + +void VirtualFileDialog::setDirBrowse() +{ + m_FileProxy->showFiles = false; + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Select Folder")); + + ui->filter->hide(); +} + +void VirtualFileDialog::keyPressEvent(QKeyEvent *e) +{ + // swallow return/enter events + if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) + return; +} + +void VirtualFileDialog::accept() +{ + // do nothing, don't accept except via our explicit calls +} + +void VirtualFileDialog::on_location_keyPress(QKeyEvent *e) +{ + // only process when enter is pressed + if(e->key() != Qt::Key_Return && e->key() != Qt::Key_Enter) + return; + + // parse folder + QModelIndex idx = m_Model->indexForPath(ui->location->text()); + + if(idx.isValid()) + changeCurrentDir(idx); + else + fileNotFound(ui->location->text()); +} + +QModelIndex VirtualFileDialog::currentDir() +{ + return m_FileProxy->mapToSource(ui->fileList->rootIndex()); +} + +void VirtualFileDialog::changeCurrentDir(const QModelIndex &index, bool recordHistory) +{ + // shouldn't happen, but sanity check + if(!index.isValid()) + return; + + // ignore changes to current dir + if(currentDir() == index) + return; + + if(recordHistory) + { + // erase the history we backed up over + while(m_History.count() > m_HistoryIndex + 1) + m_History.pop_back(); + + // add new history + m_History.push_back(index); + m_HistoryIndex = m_History.count() - 1; + } + + ui->back->setEnabled(m_HistoryIndex > 0); + ui->forward->setEnabled(m_HistoryIndex < m_History.count() - 1); + + QModelIndex fileIndex = m_FileProxy->mapFromSource(index); + QModelIndex dirIndex = m_DirProxy->mapFromSource(index); + + // set file list to this dir + ui->fileList->setRootIndex(fileIndex); + + // update location text + ui->location->setText(m_FileProxy->data(fileIndex, RemoteFileModel::FilePathRole).toString()); + + // enable up button if we're not at a root + bool isRoot = m_FileProxy->data(fileIndex, RemoteFileModel::FileIsRootRole).toBool(); + ui->upFolder->setEnabled(!isRoot); + + // expand the directory list so this directory is visible + QModelIndex parent = m_DirProxy->parent(dirIndex); + while(parent.isValid()) + { + ui->dirList->expand(parent); + parent = m_DirProxy->parent(parent); + } + + // select this directory + ui->dirList->selectionModel()->setCurrentIndex(dirIndex, QItemSelectionModel::ClearAndSelect); + + // if it was access denied, show an error now + if(m_FileProxy->data(fileIndex, RemoteFileModel::FileIsAccessDeniedRole).toBool()) + { + accessDenied(ui->location->text()); + } +} + +void VirtualFileDialog::on_dirList_clicked(const QModelIndex &index) +{ + changeCurrentDir(m_DirProxy->mapToSource(index)); +} + +void VirtualFileDialog::on_fileList_doubleClicked(const QModelIndex &index) +{ + bool isDir = m_FileProxy->data(index, RemoteFileModel::FileIsDirRole).toBool(); + + if(isDir) + { + changeCurrentDir(m_FileProxy->mapToSource(index)); + } + else + { + m_ChosenPath = m_FileProxy->data(index, RemoteFileModel::FilePathRole).toString(); + QDialog::accept(); + } +} + +void VirtualFileDialog::on_fileList_clicked(const QModelIndex &index) +{ + ui->filename->setText(m_FileProxy->data(index, RemoteFileModel::FileNameRole).toString()); +} + +void VirtualFileDialog::on_showHidden_toggled(bool checked) +{ + m_DirProxy->showHidden = ui->showHidden->isChecked(); + m_FileProxy->showHidden = ui->showHidden->isChecked(); + m_DirProxy->refresh(); + m_FileProxy->refresh(); +} + +void VirtualFileDialog::on_filename_keyPress(QKeyEvent *e) +{ + // only process when enter is pressed + if(e->key() != Qt::Key_Return && e->key() != Qt::Key_Enter) + return; + + QModelIndex curDir = ui->fileList->rootIndex(); + + QString text = ui->filename->text(); + + QRegExp re(text); + re.setPatternSyntax(QRegExp::Wildcard); + + int fileCount = m_FileProxy->rowCount(curDir); + int matches = 0; + QString match; + + for(int f = 0; f < fileCount; f++) + { + QModelIndex file = m_FileProxy->index(f, 0, curDir); + bool isDir = m_FileProxy->data(file, RemoteFileModel::FileIsDirRole).toBool(); + + if(isDir) + continue; + + QString filename = m_FileProxy->data(file, RemoteFileModel::FileNameRole).toString(); + + if(re.exactMatch(filename)) + { + matches++; + match = m_FileProxy->data(file, RemoteFileModel::FilePathRole).toString(); + } + } + + if(matches == 1) + { + m_ChosenPath = match; + QDialog::accept(); + } + + if(matches == 0 && !text.trimmed().isEmpty()) + fileNotFound(text); + + m_FileProxy->setFilterRegExp(re); + m_FileProxy->refresh(); +} + +void VirtualFileDialog::on_filter_currentIndexChanged(int index) +{ + m_FileProxy->showNonExecutables = (index == 1); + m_FileProxy->refresh(); +} + +void VirtualFileDialog::on_buttonBox_accepted() +{ + if(!m_FileProxy->showFiles) + { + // if browsing for a directory, accept current dir as path + m_ChosenPath = m_Model->data(currentDir(), RemoteFileModel::FilePathRole).toString(); + QDialog::accept(); + return; + } + + // simulate enter being pressed + QKeyEvent fakeEvent(QEvent::KeyPress, Qt::Key_Return, 0); + on_filename_keyPress(&fakeEvent); +} + +void VirtualFileDialog::on_back_clicked() +{ + m_HistoryIndex = qMax(0, m_HistoryIndex - 1); + changeCurrentDir(m_History[m_HistoryIndex], false); +} + +void VirtualFileDialog::on_forward_clicked() +{ + m_HistoryIndex = qMin(m_HistoryIndex + 1, m_History.count() - 1); + changeCurrentDir(m_History[m_HistoryIndex], false); +} + +void VirtualFileDialog::on_upFolder_clicked() +{ + QModelIndex curDir = currentDir(); + + changeCurrentDir(m_Model->parent(curDir)); +} + +void VirtualFileDialog::fileNotFound(const QString &path) +{ + RDDialog::critical(this, tr("File not found"), + tr("%1\nFile not found.\nCheck the file name and try again.").arg(path)); +} + +void VirtualFileDialog::accessDenied(const QString &path) +{ + RDDialog::critical(this, tr("Access is denied"), + tr("%1 is not accessible\n\nAccess is denied.").arg(path)); +} diff --git a/qrenderdoc/Windows/Dialogs/VirtualFileDialog.h b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.h new file mode 100644 index 000000000..dc13311d2 --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.h @@ -0,0 +1,83 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2017 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include + +namespace Ui +{ +class VirtualFileDialog; +} + +class CaptureContext; +class RemoteFileModel; +class RemoteFileProxy; + +class VirtualFileDialog : public QDialog +{ + Q_OBJECT + +public: + explicit VirtualFileDialog(CaptureContext *ctx, QWidget *parent = 0); + ~VirtualFileDialog(); + + QString chosenPath() { return m_ChosenPath; } + void setDirBrowse(); + +private slots: + // automatic slots + void on_dirList_clicked(const QModelIndex &index); + void on_fileList_doubleClicked(const QModelIndex &index); + void on_fileList_clicked(const QModelIndex &index); + void on_showHidden_toggled(bool checked); + void on_filename_keyPress(QKeyEvent *e); + void on_filter_currentIndexChanged(int index); + void on_location_keyPress(QKeyEvent *e); + void on_buttonBox_accepted(); + void on_back_clicked(); + void on_forward_clicked(); + void on_upFolder_clicked(); + +private: + Ui::VirtualFileDialog *ui; + + QString m_ChosenPath; + + RemoteFileModel *m_Model; + RemoteFileProxy *m_DirProxy; + RemoteFileProxy *m_FileProxy; + + QList m_History; + int m_HistoryIndex = 0; + + void keyPressEvent(QKeyEvent *e) override; + void accept() override; + + QModelIndex currentDir(); + void changeCurrentDir(const QModelIndex &index, bool recordHistory = true); + + void fileNotFound(const QString &path); + void accessDenied(const QString &path); +}; diff --git a/qrenderdoc/Windows/Dialogs/VirtualFileDialog.ui b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.ui new file mode 100644 index 000000000..4d14205ca --- /dev/null +++ b/qrenderdoc/Windows/Dialogs/VirtualFileDialog.ui @@ -0,0 +1,272 @@ + + + VirtualFileDialog + + + + 0 + 0 + 594 + 454 + + + + Dialog + + + + + + + + ... + + + + :/Resources/back.png:/Resources/back.png + + + + 24 + 24 + + + + true + + + + + + + ... + + + + :/Resources/forward.png:/Resources/forward.png + + + + 24 + 24 + + + + true + + + + + + + Qt::Vertical + + + + + + + ... + + + + :/Resources/upfolder.png:/Resources/upfolder.png + + + + 24 + 24 + + + + true + + + + + + + Qt::Vertical + + + + + + + Location: + + + + + + + + + + + + + + + 1 + 0 + + + + false + + + true + + + + + + + + 2 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + 0 + + + false + + + false + + + true + + + true + + + true + + + false + + + + + + + + + + + Filename: + + + + + + + + 3 + 0 + + + + + + + + + 1 + 0 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Show hidden files + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + + + RDLineEdit + QLineEdit +
Widgets/Extended/RDLineEdit.h
+
+
+ + + + + + buttonBox + rejected() + VirtualFileDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/qrenderdoc/qrenderdoc.pro b/qrenderdoc/qrenderdoc.pro index 627882e32..52271d556 100644 --- a/qrenderdoc/qrenderdoc.pro +++ b/qrenderdoc/qrenderdoc.pro @@ -138,7 +138,8 @@ SOURCES += Code/qrenderdoc.cpp \ Windows/Dialogs/SettingsDialog.cpp \ Windows/Dialogs/OrderedListEditor.cpp \ Widgets/Extended/RDTableWidget.cpp \ - Windows/Dialogs/SuggestRemoteDialog.cpp + Windows/Dialogs/SuggestRemoteDialog.cpp \ + Windows/Dialogs/VirtualFileDialog.cpp HEADERS += Code/CaptureContext.h \ Code/qprocessinfo.h \ @@ -181,7 +182,8 @@ HEADERS += Code/CaptureContext.h \ Windows/Dialogs/SettingsDialog.h \ Windows/Dialogs/OrderedListEditor.h \ Widgets/Extended/RDTableWidget.h \ - Windows/Dialogs/SuggestRemoteDialog.h + Windows/Dialogs/SuggestRemoteDialog.h \ + Windows/Dialogs/VirtualFileDialog.h FORMS += Windows/Dialogs/AboutDialog.ui \ Windows/MainWindow.ui \ @@ -206,7 +208,8 @@ FORMS += Windows/Dialogs/AboutDialog.ui \ Windows/StatisticsViewer.ui \ Windows/Dialogs/SettingsDialog.ui \ Windows/Dialogs/OrderedListEditor.ui \ - Windows/Dialogs/SuggestRemoteDialog.ui + Windows/Dialogs/SuggestRemoteDialog.ui \ + Windows/Dialogs/VirtualFileDialog.ui RESOURCES += resources.qrc diff --git a/qrenderdoc/qrenderdoc_local.vcxproj b/qrenderdoc/qrenderdoc_local.vcxproj index de4fc3d0f..a16f9d89f 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj +++ b/qrenderdoc/qrenderdoc_local.vcxproj @@ -533,6 +533,7 @@ + @@ -560,6 +561,7 @@ + Level3 @@ -679,6 +681,7 @@ + @@ -706,6 +709,7 @@ + @@ -738,6 +742,7 @@ + diff --git a/qrenderdoc/qrenderdoc_local.vcxproj.filters b/qrenderdoc/qrenderdoc_local.vcxproj.filters index 205242c11..c9a3d53b5 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj.filters +++ b/qrenderdoc/qrenderdoc_local.vcxproj.filters @@ -505,6 +505,12 @@ Generated Files + + Windows\Dialogs + + + Generated Files + @@ -900,6 +906,12 @@ Generated Files + + Windows\Dialogs + + + Generated Files + @@ -1147,6 +1159,9 @@ Windows\Dialogs + + Windows\Dialogs +