├── qsshfs.ico ├── qsshfs_16.png ├── qsshfs_32.png ├── mountinfo.cpp ├── res.qrc ├── de.skycoder42.qsshfs.desktop ├── qpm.json ├── mountinfo.h ├── README.md ├── .gitignore ├── main.cpp ├── editremotedialog.h ├── mountcontroller.h ├── qsshfs.pro ├── mainwindow.h ├── mountmodel.h ├── LICENSE ├── qsshfs-installer.sh ├── editremotedialog.cpp ├── mountcontroller.cpp ├── editremotedialog.ui ├── mountmodel.cpp ├── mainwindow.cpp └── mainwindow.ui /qsshfs.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skycoder42/qsshfs/HEAD/qsshfs.ico -------------------------------------------------------------------------------- /qsshfs_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skycoder42/qsshfs/HEAD/qsshfs_16.png -------------------------------------------------------------------------------- /qsshfs_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skycoder42/qsshfs/HEAD/qsshfs_32.png -------------------------------------------------------------------------------- /mountinfo.cpp: -------------------------------------------------------------------------------- 1 | #include "mountinfo.h" 2 | 3 | bool MountInfo::isValid() const 4 | { 5 | return !name.isNull(); 6 | } 7 | -------------------------------------------------------------------------------- /res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qsshfs.ico 4 | 5 | 6 | -------------------------------------------------------------------------------- /de.skycoder42.qsshfs.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.1 4 | Name=Qt sshfs GUI 5 | Comment=A gui wrapper around sshfs, written in Qt 6 | Exec=qsshfs 7 | Icon=qsshfs 8 | Terminal=false 9 | Categories=Development;Qt; -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "dependencies": [ 5 | "de.skycoder42.dialog-master@1.2.5", 6 | "de.skycoder42.qpathedit@2.0.0" 7 | ], 8 | "license": "NONE", 9 | "pri_filename": "", 10 | "webpage": "" 11 | } -------------------------------------------------------------------------------- /mountinfo.h: -------------------------------------------------------------------------------- 1 | #ifndef MOUNTINFO_H 2 | #define MOUNTINFO_H 3 | 4 | #include 5 | 6 | struct MountInfo 7 | { 8 | QString name; 9 | QString hostName; 10 | QString userOverwrite; 11 | QString remotePath; 12 | QString localPath; 13 | 14 | bool isValid() const; 15 | }; 16 | 17 | #endif // MOUNTINFO_H 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qsshfs 2 | A gui wrapper around sshfs 3 | 4 | Allows you to easily manage connected sshfs mountpoints via a simply gui, and via your systemtray. You can create predefined mounts by combining the tool with the ssh config file. 5 | 6 | ## Installation 7 | AUR-Package: https://aur.archlinux.org/packages/qsshfs/ 8 | 9 | ## Icons 10 | - https://www.iconfinder.com/icons/30483/folder_remote_ssh_icon -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | # QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | # QtCtreator CMake 37 | CMakeLists.txt.user 38 | 39 | # qpm 40 | vendor -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QApplication a(argc, argv); 7 | QApplication::setApplicationName(QStringLiteral(TARGET)); 8 | QApplication::setApplicationVersion(QStringLiteral(VERSION)); 9 | QApplication::setOrganizationName(QStringLiteral(COMPANY)); 10 | QApplication::setOrganizationDomain(QStringLiteral(BUNDLE_PREFIX)); 11 | QApplication::setApplicationDisplayName(QStringLiteral(APPNAME)); 12 | QApplication::setWindowIcon(QIcon(QStringLiteral(":/icons/main.ico"))); 13 | QApplication::setFallbackSessionManagementEnabled(false); 14 | 15 | MainWindow w; 16 | if(!QApplication::arguments().contains(QStringLiteral("--hidden"))) 17 | w.show(); 18 | 19 | return a.exec(); 20 | } 21 | -------------------------------------------------------------------------------- /editremotedialog.h: -------------------------------------------------------------------------------- 1 | #ifndef EDITREMOTEDIALOG_H 2 | #define EDITREMOTEDIALOG_H 3 | 4 | #include "mountinfo.h" 5 | 6 | #include 7 | 8 | namespace Ui { 9 | class EditRemoteDialog; 10 | } 11 | 12 | class EditRemoteDialog : public QDialog 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | static MountInfo editInfo(const MountInfo &oldInfo = {}, QWidget *parent = nullptr); 18 | 19 | private slots: 20 | void on_nameLineEdit_textChanged(const QString &text); 21 | void on_hostnameComboBox_editTextChanged(const QString &text); 22 | void on_editConfigButton_clicked(); 23 | 24 | 25 | private: 26 | Ui::EditRemoteDialog *ui; 27 | 28 | explicit EditRemoteDialog(QWidget *parent = nullptr); 29 | ~EditRemoteDialog(); 30 | 31 | void readSshConfig(const QString &fileName); 32 | }; 33 | 34 | #endif // EDITREMOTEDIALOG_H 35 | -------------------------------------------------------------------------------- /mountcontroller.h: -------------------------------------------------------------------------------- 1 | #ifndef MOUNTCONTROLLER_H 2 | #define MOUNTCONTROLLER_H 3 | 4 | #include "mountinfo.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | class MountController : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit MountController(QObject *parent = nullptr); 16 | 17 | MountInfo mountInfo(const QString &name) const; 18 | bool isMounted(const QString &name); 19 | 20 | public slots: 21 | void addMount(const MountInfo &info); 22 | void removeMount(const QString &name); 23 | void reloadState(); 24 | 25 | void mount(const QString &name); 26 | void unmount(const QString &name); 27 | 28 | signals: 29 | void mountChanged(const QString &name); 30 | void mountError(const QString &name, const QString &errorLog, int exitCode = -1); 31 | 32 | private: 33 | struct MountState 34 | { 35 | MountInfo info; 36 | bool mounted; 37 | QPointer process; 38 | 39 | MountState(const MountInfo &info = {}); 40 | }; 41 | 42 | QHash _mounts; 43 | 44 | QProcess *createProcess(const QString &name, bool forMount); 45 | }; 46 | 47 | #endif // MOUNTCONTROLLER_H 48 | -------------------------------------------------------------------------------- /qsshfs.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += core gui widgets 4 | 5 | TARGET = qsshfs 6 | APPNAME = "Qt sshfs GUI" 7 | VERSION = 1.1.0 8 | COMPANY = Skycoder42 9 | BUNDLE_PREFIX = de.skycoder42 10 | 11 | DEFINES += "TARGET=\\\"$$TARGET\\\"" 12 | DEFINES += "\"APPNAME=\\\"$$APPNAME\\\"\"" 13 | DEFINES += "VERSION=\\\"$$VERSION\\\"" 14 | DEFINES += "COMPANY=\\\"$$COMPANY\\\"" 15 | DEFINES += "BUNDLE_PREFIX=\\\"$$BUNDLE_PREFIX\\\"" 16 | 17 | DEFINES += QT_DEPRECATED_WARNINGS 18 | DEFINES += QT_ASCII_CAST_WARNINGS 19 | 20 | include(vendor/vendor.pri) 21 | 22 | HEADERS += mainwindow.h \ 23 | editremotedialog.h \ 24 | mountmodel.h \ 25 | mountinfo.h \ 26 | mountcontroller.h 27 | 28 | SOURCES += main.cpp\ 29 | mainwindow.cpp \ 30 | editremotedialog.cpp \ 31 | mountmodel.cpp \ 32 | mountinfo.cpp \ 33 | mountcontroller.cpp 34 | 35 | FORMS += mainwindow.ui \ 36 | editremotedialog.ui 37 | 38 | RESOURCES += \ 39 | res.qrc 40 | 41 | win32 { 42 | QMAKE_TARGET_PRODUCT = $$APPNAME 43 | QMAKE_TARGET_COMPANY = $$COMPANY 44 | QMAKE_TARGET_COPYRIGHT = "Felix Barz" 45 | } else:mac { 46 | QMAKE_TARGET_BUNDLE_PREFIX = $${BUNDLE_PREFIX}. 47 | } 48 | 49 | target.path = $$[QT_INSTALL_BINS] 50 | INSTALLS += target 51 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "mountmodel.h" 9 | 10 | namespace Ui { 11 | class MainWindow; 12 | } 13 | 14 | class MainWindow : public QMainWindow 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit MainWindow(QWidget *parent = nullptr); 20 | ~MainWindow(); 21 | 22 | private slots: 23 | void mountError(const QString &name, const QString &errorLog, int exitCode); 24 | 25 | void reloadCurrent(const QModelIndex &uiIndex); 26 | void updateAutostart(bool checked); 27 | 28 | void commitShutdown(QSessionManager &sm); 29 | 30 | void on_actionAdd_Host_triggered(); 31 | void on_actionEdit_Host_triggered(); 32 | void on_actionRemove_Host_triggered(); 33 | void on_actionMount_triggered(bool checked); 34 | void on_actionOpen_Folder_triggered(); 35 | void on_actionAbout_triggered(); 36 | 37 | void on_treeView_activated(const QModelIndex &index); 38 | 39 | private: 40 | Ui::MainWindow *ui; 41 | MountModel *model; 42 | QSortFilterProxyModel *sortModel; 43 | 44 | QSystemTrayIcon *trayIco; 45 | 46 | bool isAutostart(); 47 | }; 48 | 49 | #endif // MAINWINDOW_H 50 | -------------------------------------------------------------------------------- /mountmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef MOUNTMODEL_H 2 | #define MOUNTMODEL_H 3 | 4 | #include 5 | #include 6 | #include "mountinfo.h" 7 | #include "mountcontroller.h" 8 | 9 | class MountModel : public QAbstractTableModel 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit MountModel(QObject *parent = nullptr); 15 | 16 | MountController *controller(); 17 | 18 | QMenu *createMountMenu(QWidget *parent); 19 | 20 | MountInfo mountInfo(const QModelIndex &index) const; 21 | void addMountInfo(const MountInfo &info); 22 | void updateMountInfo(const QModelIndex &index, const MountInfo &info); 23 | void removeMountInfo(const QModelIndex &index); 24 | 25 | bool isMounted(const QModelIndex &index) const; 26 | void mount(const QModelIndex &index); 27 | void unmount(const QModelIndex &index); 28 | void reload(); 29 | 30 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 31 | 32 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 33 | int columnCount(const QModelIndex &parent = QModelIndex()) const override; 34 | 35 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 36 | 37 | private slots: 38 | void updateMounted(const QString &name); 39 | 40 | void triggered(bool checked); 41 | 42 | private: 43 | MountController *_controller; 44 | QStringList _names; 45 | 46 | QMenu *_mntMenu; 47 | QHash _mntActions; 48 | 49 | void addMntAction(const QString &name); 50 | void saveState(); 51 | }; 52 | 53 | #endif // MOUNTMODEL_H 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Felix Barz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /qsshfs-installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TMPDIR="/var/tmp/qsshfs-installer" 4 | QSSHFS_DIR="$TMPDIR/qsshfs" 5 | QPM_DIR="$TMPDIR/qpm" 6 | QSSHFS="https://github.com/Skycoder42/qsshfs.git" 7 | QSSHFS_VER="1.1.0" 8 | QPM_VER="0.11.0" 9 | export GOPATH="$QPM_DIR/gopath" 10 | QPM="$GOPATH/bin/qpm" 11 | 12 | src_fetch() { 13 | git clone $QSSHFS $QSSHFS_DIR 14 | } 15 | 16 | src_prepare() { 17 | cd $QSSHFS_DIR 18 | git reset --hard $QSSHFS_VER 19 | } 20 | 21 | make_dirs() { 22 | mkdir -p $TMPDIR 23 | mkdir -p $QSSHFS_DIR 24 | mkdir -p $QPM_DIR 25 | mkdir -p $GOPATH 26 | } 27 | 28 | clean() { 29 | rm -Rf $TMPDIR 30 | } 31 | 32 | make_qpm() { 33 | echo "This is the Gopath: $GOPATH" 34 | go get qpm.io/qpm 35 | cd $GOPATH/src/qpm.io 36 | git submodule init 37 | git submodule update 38 | go install qpm.io/qpm 39 | unset GOPATH 40 | } 41 | 42 | make_qsshfs() { 43 | cd $QSSHFS_DIR 44 | $QPM install 45 | qmake -r "./" 46 | make -j"$(nproc --all)" 47 | } 48 | 49 | install_qsshfs() { 50 | cd $QSSHFS_DIR 51 | sudo make install 52 | sudo ln -s /usr/lib64/qt5/bin/qsshfs /usr/bin/qsshfs 53 | sudo install -D -m644 de.skycoder42.qsshfs.desktop "/usr/share/applications/de.skycoder42.qsshfs.desktop" 54 | sudo install -D -m644 qsshfs_32.png "/usr/share/icons/hicolor/32x32/apps/qsshfs.png" 55 | sudo install -D -m644 qsshfs_16.png "/usr/share/icons/hicolor/16x16/apps/qsshfs.png" 56 | sudo install -D -m755 $0 "/usr/bin/qsshfs-installer" 57 | } 58 | 59 | installer() { 60 | 61 | clean 62 | src_fetch 63 | src_prepare 64 | make_qpm 65 | make_qsshfs 66 | install_qsshfs 67 | } 68 | 69 | uninstaller() { 70 | sudo rm -f /usr/lib64/qt5/bin/qsshfs 71 | sudo rm -f /usr/share/applications/de.skycoder42.qsshfs.desktop 72 | sudo rm -f /usr/share/icons/hicolor/32x32/apps/qsshfs.png 73 | sudo rm -f /usr/share/icons/hicolor/16x16/apps/qsshfs.png 74 | } 75 | 76 | help() { 77 | echo "This is the installer script for qsshfs. This will fetch, compile and install qsshfs by itself." 78 | echo " "install": Will compile and install qsshfs." 79 | echo " "uninstall": Will remove the installed version of qsshfs." 80 | } 81 | 82 | for i in "$@" 83 | do 84 | case $i in 85 | install) 86 | installer 87 | ;; 88 | uninstall) 89 | uninstaller 90 | ;; 91 | -h | --help | help) 92 | help 93 | ;; 94 | esac 95 | done 96 | -------------------------------------------------------------------------------- /editremotedialog.cpp: -------------------------------------------------------------------------------- 1 | #include "editremotedialog.h" 2 | #include "ui_editremotedialog.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | MountInfo EditRemoteDialog::editInfo(const MountInfo &oldInfo, QWidget *parent) 10 | { 11 | EditRemoteDialog dialog(parent); 12 | 13 | if(oldInfo.isValid()) { 14 | dialog.ui->nameLineEdit->setText(oldInfo.name); 15 | dialog.ui->nameLineEdit->setEnabled(false); 16 | dialog.ui->hostnameComboBox->setCurrentText(oldInfo.hostName); 17 | dialog.ui->userLineEdit->setText(oldInfo.userOverwrite); 18 | dialog.ui->remoteMountpointLineEdit->setText(oldInfo.remotePath); 19 | dialog.ui->localMountpointPathEdit->setPath(oldInfo.localPath); 20 | } 21 | 22 | if(dialog.exec() == QDialog::Accepted) { 23 | MountInfo info; 24 | info.name = dialog.ui->nameLineEdit->text(); 25 | info.hostName = dialog.ui->hostnameComboBox->currentText(); 26 | info.userOverwrite = dialog.ui->userLineEdit->text(); 27 | info.remotePath = dialog.ui->remoteMountpointLineEdit->text(); 28 | if(info.remotePath.isEmpty()) 29 | info.remotePath = QStringLiteral("/"); 30 | info.localPath = dialog.ui->localMountpointPathEdit->path(); 31 | if(info.localPath.isEmpty()) 32 | info.localPath = dialog.ui->localMountpointPathEdit->placeholder(); 33 | return info; 34 | } else 35 | return {}; 36 | } 37 | 38 | EditRemoteDialog::EditRemoteDialog(QWidget *parent) : 39 | QDialog(parent), 40 | ui(new Ui::EditRemoteDialog) 41 | { 42 | DialogMaster::masterDialog(this, true); 43 | ui->setupUi(this); 44 | 45 | QDir home = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); 46 | readSshConfig(QStringLiteral("/etc/ssh/ssh_config")); 47 | readSshConfig(home.absoluteFilePath(QStringLiteral(".ssh/config"))); 48 | 49 | home.mkdir(QStringLiteral("mnt")); 50 | ui->localMountpointPathEdit->setDefaultDirectory(home.absoluteFilePath(QStringLiteral("mnt"))); 51 | 52 | on_nameLineEdit_textChanged({}); 53 | on_hostnameComboBox_editTextChanged(ui->hostnameComboBox->currentText()); 54 | } 55 | 56 | EditRemoteDialog::~EditRemoteDialog() 57 | { 58 | delete ui; 59 | } 60 | 61 | void EditRemoteDialog::on_nameLineEdit_textChanged(const QString &text) 62 | { 63 | ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); 64 | } 65 | 66 | void EditRemoteDialog::on_hostnameComboBox_editTextChanged(const QString &text) 67 | { 68 | QDir home = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); 69 | auto path = home.absoluteFilePath(QStringLiteral("mnt/") + text); 70 | ui->localMountpointPathEdit->setPlaceholder(path); 71 | } 72 | 73 | void EditRemoteDialog::on_editConfigButton_clicked() 74 | { 75 | QDir home = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); 76 | QDesktopServices::openUrl(QUrl::fromLocalFile(home.absoluteFilePath(QStringLiteral("./.ssh/config")))); 77 | } 78 | 79 | void EditRemoteDialog::readSshConfig(const QString &fileName) 80 | { 81 | QFile sshConfig(fileName); 82 | if(sshConfig.open(QIODevice::ReadOnly | QIODevice::Text)) { 83 | QTextStream stream(&sshConfig); 84 | QString line; 85 | while(stream.readLineInto(&line)) { 86 | if(line.startsWith(QStringLiteral("Host "))) 87 | ui->hostnameComboBox->addItem(line.mid(5).trimmed()); 88 | } 89 | sshConfig.close(); 90 | } else { 91 | DialogMaster::warning(parentWidget(), 92 | tr("Failed to read ssh config file \"%1\" with error: \"%2\"\n" 93 | "Make sure the file exists and is readable by this user!") 94 | .arg(fileName) 95 | .arg(sshConfig.errorString())); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mountcontroller.cpp: -------------------------------------------------------------------------------- 1 | #include "mountcontroller.h" 2 | 3 | #include 4 | #include 5 | 6 | MountController::MountController(QObject *parent) : 7 | QObject(parent), 8 | _mounts() 9 | {} 10 | 11 | MountInfo MountController::mountInfo(const QString &name) const 12 | { 13 | return _mounts.value(name).info; 14 | } 15 | 16 | bool MountController::isMounted(const QString &name) 17 | { 18 | return _mounts.value(name).mounted; 19 | } 20 | 21 | void MountController::addMount(const MountInfo &info) 22 | { 23 | _mounts.insert(info.name, info); 24 | 25 | //check mount state 26 | QProcess state; 27 | state.start(QStringLiteral("mount -l -t fuse.sshfs")); 28 | if(state.waitForFinished(1000)) { 29 | auto data = state.readAll(); 30 | if(data.contains(QDir::cleanPath(info.localPath).toUtf8())) 31 | _mounts[info.name].mounted = true; 32 | } else 33 | qWarning() << "Unable to get mount state"; 34 | } 35 | 36 | void MountController::removeMount(const QString &name) 37 | { 38 | _mounts.remove(name); 39 | } 40 | 41 | void MountController::reloadState() 42 | { 43 | QProcess state; 44 | state.start(QStringLiteral("mount -l -t fuse.sshfs")); 45 | if(state.waitForFinished(1000)) { 46 | auto data = state.readAll(); 47 | for(auto it = _mounts.begin(); it != _mounts.end(); it++) { 48 | it->mounted = data.contains(QDir::cleanPath(it->info.localPath).toUtf8()); 49 | emit mountChanged(it->info.name); 50 | } 51 | } else 52 | qWarning() << "Unable to get mount state"; 53 | } 54 | 55 | void MountController::mount(const QString &name) 56 | { 57 | if(!_mounts.contains(name)) 58 | return; 59 | auto &state = _mounts[name]; 60 | if(state.mounted) 61 | return; 62 | 63 | if(state.process) { 64 | emit mountChanged(name); 65 | emit mountError(name, tr("Wait for the previous mount/unmount to finish")); 66 | return; 67 | } 68 | auto env = QProcessEnvironment::systemEnvironment(); 69 | if(!env.contains(QStringLiteral("SSH_ASKPASS"))) { 70 | emit mountChanged(name); 71 | emit mountError(name, tr("The SSH_ASKPASS environment variable must be set!\n" 72 | "You can use for example \"ksshaskpass\"")); 73 | return; 74 | } 75 | 76 | QDir mntDir(state.info.localPath); 77 | if(!mntDir.exists()){ 78 | if(!mntDir.mkpath(QStringLiteral("."))) { 79 | emit mountChanged(name); 80 | emit mountError(name, 81 | tr("Failed to create mount directory %1") 82 | .arg(mntDir.absolutePath())); 83 | return; 84 | } 85 | } 86 | 87 | state.process = createProcess(name, true); 88 | state.process->setProgram(QStringLiteral("sshfs")); 89 | QStringList args; 90 | args.append(state.info.hostName + QLatin1Char(':') + state.info.remotePath); 91 | args.append(state.info.localPath); 92 | state.process->setArguments(args); 93 | 94 | state.process->start(); 95 | } 96 | 97 | void MountController::unmount(const QString &name) 98 | { 99 | if(!_mounts.contains(name)) 100 | return; 101 | auto &state = _mounts[name]; 102 | if(!state.mounted) 103 | return; 104 | 105 | if(state.process) { 106 | emit mountChanged(name); 107 | emit mountError(name, tr("Wait for the previous mount/unmount to finish")); 108 | return; 109 | } 110 | 111 | state.process = createProcess(name, false); 112 | state.process->setProgram(QStringLiteral("fusermount")); 113 | QStringList args; 114 | args.append(QStringLiteral("-u")); 115 | args.append(state.info.localPath); 116 | state.process->setArguments(args); 117 | 118 | state.process->start(); 119 | } 120 | 121 | QProcess *MountController::createProcess(const QString &name, bool forMount) 122 | { 123 | auto process = new QProcess(this); 124 | process->setProcessChannelMode(QProcess::MergedChannels); 125 | connect(process, QOverload::of(&QProcess::finished), 126 | this, [=](int exitCode, QProcess::ExitStatus exitStatus){ 127 | if(exitStatus != QProcess::NormalExit) { 128 | emit mountChanged(name); 129 | emit mountError(name, process->errorString()); 130 | } else if(exitCode != EXIT_SUCCESS) { 131 | emit mountChanged(name); 132 | emit mountError(name, QString::fromUtf8(process->readAll()), exitCode); 133 | } else { 134 | _mounts[name].mounted = forMount; 135 | emit mountChanged(name); 136 | } 137 | process->deleteLater(); 138 | }); 139 | return process; 140 | } 141 | 142 | MountController::MountState::MountState(const MountInfo &info) : 143 | info(info), 144 | mounted(false), 145 | process(nullptr) 146 | {} 147 | -------------------------------------------------------------------------------- /editremotedialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EditRemoteDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 416 10 | 230 11 | 12 | 13 | 14 | Edit Mount Config 15 | 16 | 17 | 18 | 19 | 20 | &Name: 21 | 22 | 23 | nameLineEdit 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | &Hostname: 34 | 35 | 36 | hostnameComboBox 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | &User: 62 | 63 | 64 | userLineEdit 65 | 66 | 67 | 68 | 69 | 70 | 71 | leave empty for user from config 72 | 73 | 74 | 75 | 76 | 77 | 78 | &Remote Mountpoint: 79 | 80 | 81 | remoteMountpointLineEdit 82 | 83 | 84 | 85 | 86 | 87 | 88 | / 89 | 90 | 91 | 92 | 93 | 94 | 95 | &Local Mountpoint: 96 | 97 | 98 | localMountpointPathEdit 99 | 100 | 101 | 102 | 103 | 104 | 105 | QPathEdit::JoinedButton 106 | 107 | 108 | 109 | .. 110 | 111 | 112 | QPathEdit::ExistingFolder 113 | 114 | 115 | QFileDialog::DontResolveSymlinks|QFileDialog::ShowDirsOnly 116 | 117 | 118 | true 119 | 120 | 121 | 122 | 123 | 124 | 125 | &Mount: 126 | 127 | 128 | mountCheckBox 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Qt::Horizontal 139 | 140 | 141 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | QPathEdit 150 | QWidget 151 |
qpathedit.h
152 |
153 |
154 | 155 | 156 | 157 | buttonBox 158 | accepted() 159 | EditRemoteDialog 160 | accept() 161 | 162 | 163 | 248 164 | 254 165 | 166 | 167 | 157 168 | 274 169 | 170 | 171 | 172 | 173 | buttonBox 174 | rejected() 175 | EditRemoteDialog 176 | reject() 177 | 178 | 179 | 316 180 | 260 181 | 182 | 183 | 286 184 | 274 185 | 186 | 187 | 188 | 189 |
190 | -------------------------------------------------------------------------------- /mountmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "mountmodel.h" 2 | 3 | #include 4 | 5 | MountModel::MountModel(QObject *parent) : 6 | QAbstractTableModel(parent), 7 | _controller(new MountController(this)), 8 | _names(), 9 | _mntMenu(nullptr), 10 | _mntActions() 11 | { 12 | connect(_controller, &MountController::mountChanged, 13 | this, &MountModel::updateMounted); 14 | 15 | QSettings settings; 16 | auto max = settings.beginReadArray(QStringLiteral("mounts")); 17 | for(auto i = 0; i < max; i++) { 18 | MountInfo data; 19 | settings.setArrayIndex(i); 20 | data.name = settings.value(QStringLiteral("name")).toString(); 21 | data.hostName = settings.value(QStringLiteral("hostName")).toString(); 22 | data.userOverwrite = settings.value(QStringLiteral("userOverwrite")).toString(); 23 | data.remotePath = settings.value(QStringLiteral("remotePath")).toString(); 24 | data.localPath = settings.value(QStringLiteral("localPath")).toString(); 25 | _controller->addMount(data); 26 | _names.append(data.name); 27 | } 28 | settings.endArray(); 29 | } 30 | 31 | MountController *MountModel::controller() 32 | { 33 | return _controller; 34 | } 35 | 36 | QMenu *MountModel::createMountMenu(QWidget *parent) 37 | { 38 | if(!_mntMenu) { 39 | _mntMenu = new QMenu(tr("Mounts"), parent); 40 | _mntMenu->setIcon(QIcon::fromTheme(QStringLiteral("gtk-connect"))); 41 | 42 | foreach (auto name, _names) 43 | addMntAction(name); 44 | } 45 | 46 | return _mntMenu; 47 | } 48 | 49 | MountInfo MountModel::mountInfo(const QModelIndex &index) const 50 | { 51 | if (!index.isValid() || 52 | index.row() < 0 || 53 | index.row() >= _names.size()) 54 | return {}; 55 | else 56 | return _controller->mountInfo(_names[index.row()]); 57 | } 58 | 59 | void MountModel::addMountInfo(const MountInfo &info) 60 | { 61 | if(_names.contains(info.name)) { 62 | //TODO show error 63 | return; 64 | } 65 | 66 | beginInsertRows(QModelIndex(), _names.size(), _names.size()); 67 | _controller->addMount(info); 68 | _names.append(info.name); 69 | addMntAction(info.name); 70 | endInsertRows(); 71 | 72 | saveState(); 73 | } 74 | 75 | void MountModel::updateMountInfo(const QModelIndex &index, const MountInfo &info) 76 | { 77 | if (!index.isValid() || 78 | index.row() < 0 || 79 | index.row() >= _names.size()) 80 | return; 81 | 82 | if(_names[index.row()] != info.name) { 83 | //TODO show error 84 | return; 85 | } 86 | if(_controller->isMounted(info.name)) { 87 | //TODO show error 88 | return; 89 | } 90 | 91 | _controller->removeMount(_names[index.row()]); 92 | _controller->addMount(info); 93 | emit dataChanged(index.sibling(index.row(), 0), 94 | index.sibling(index.row(), 2)); 95 | 96 | saveState(); 97 | } 98 | 99 | void MountModel::removeMountInfo(const QModelIndex &index) 100 | { 101 | if (!index.isValid() || 102 | index.row() < 0 || 103 | index.row() >= _names.size()) 104 | return; 105 | 106 | auto name = _names[index.row()]; 107 | if(_controller->isMounted(name)) { 108 | //TODO show error 109 | return; 110 | } 111 | 112 | beginRemoveRows(index.parent(), index.row(), index.row()); 113 | auto act = _mntActions.take(name); 114 | if(act) 115 | act->deleteLater(); 116 | _names.removeAt(index.row()); 117 | _controller->removeMount(name); 118 | endRemoveRows(); 119 | 120 | saveState(); 121 | } 122 | 123 | bool MountModel::isMounted(const QModelIndex &index) const 124 | { 125 | if (!index.isValid() || 126 | index.row() < 0 || 127 | index.row() >= _names.size()) 128 | return false; 129 | else 130 | return _controller->isMounted(_names[index.row()]); 131 | } 132 | 133 | void MountModel::mount(const QModelIndex &index) 134 | { 135 | if (!index.isValid() || 136 | index.row() < 0 || 137 | index.row() >= _names.size()) 138 | return; 139 | 140 | _controller->mount(_names[index.row()]); 141 | } 142 | 143 | void MountModel::unmount(const QModelIndex &index) 144 | { 145 | if (!index.isValid() || 146 | index.row() < 0 || 147 | index.row() >= _names.size()) 148 | return; 149 | 150 | _controller->unmount(_names[index.row()]); 151 | } 152 | 153 | void MountModel::reload() 154 | { 155 | beginResetModel(); 156 | _controller->reloadState(); 157 | endResetModel(); 158 | } 159 | 160 | QVariant MountModel::headerData(int section, Qt::Orientation orientation, int role) const 161 | { 162 | if(orientation != Qt::Horizontal || role != Qt::DisplayRole) 163 | return {}; 164 | 165 | switch (section) { 166 | case 0: 167 | return tr("Name"); 168 | case 1: 169 | return tr("Host"); 170 | case 2: 171 | return tr("Mounted"); 172 | default: 173 | Q_UNREACHABLE(); 174 | return {}; 175 | } 176 | } 177 | 178 | int MountModel::rowCount(const QModelIndex &parent) const 179 | { 180 | if (parent.isValid()) 181 | return 0; 182 | else 183 | return _names.size(); 184 | } 185 | 186 | int MountModel::columnCount(const QModelIndex &parent) const 187 | { 188 | if (parent.isValid()) 189 | return 0; 190 | else 191 | return 3; 192 | } 193 | 194 | QVariant MountModel::data(const QModelIndex &index, int role) const 195 | { 196 | if (!index.isValid() || 197 | index.row() < 0 || 198 | index.row() >= _names.size()) 199 | return {}; 200 | 201 | auto data = _controller->mountInfo(_names[index.row()]); 202 | switch (index.column()) { 203 | case 0: 204 | if(role == Qt::DisplayRole) 205 | return data.name; 206 | break; 207 | case 1: 208 | if(role == Qt::DisplayRole) { 209 | auto host = data.hostName; 210 | if(!data.userOverwrite.isEmpty()) 211 | host.prepend(data.userOverwrite + QLatin1Char('@')); 212 | return host; 213 | } 214 | break; 215 | case 2: 216 | if(role == Qt::CheckStateRole) 217 | return _controller->isMounted(data.name) ? Qt::Checked : Qt::Unchecked; 218 | else if(role == Qt::DisplayRole) 219 | return _controller->isMounted(data.name) ? data.localPath : QString(); 220 | break; 221 | default: 222 | break; 223 | } 224 | 225 | return {}; 226 | } 227 | 228 | void MountModel::updateMounted(const QString &name) 229 | { 230 | auto mIndex = index(_names.indexOf(name), 2); 231 | if(mIndex.isValid()) 232 | emit dataChanged(mIndex, mIndex, {Qt::CheckStateRole}); 233 | 234 | auto act = _mntActions.value(name); 235 | if(act) 236 | act->setChecked(_controller->isMounted(name)); 237 | } 238 | 239 | void MountModel::triggered(bool checked) 240 | { 241 | auto act = qobject_cast(sender()); 242 | if(act) { 243 | auto name = _mntActions.key(act); 244 | if(!name.isEmpty()) { 245 | if(checked) 246 | _controller->mount(name); 247 | else 248 | _controller->unmount(name); 249 | act->setChecked(!checked); 250 | } 251 | } 252 | } 253 | 254 | void MountModel::addMntAction(const QString &name) 255 | { 256 | if(_mntMenu) { 257 | auto act = _mntMenu->addAction(name); 258 | act->setCheckable(true); 259 | act->setChecked(_controller->isMounted(name)); 260 | connect(act, &QAction::triggered, 261 | this, &MountModel::triggered); 262 | _mntActions.insert(name, act); 263 | } 264 | } 265 | 266 | void MountModel::saveState() 267 | { 268 | QSettings settings; 269 | settings.beginWriteArray(QStringLiteral("mounts"), _names.size()); 270 | for(auto i = 0; i < _names.size(); i++) { 271 | const auto &data = _controller->mountInfo(_names[i]); 272 | settings.setArrayIndex(i); 273 | settings.setValue(QStringLiteral("name"), data.name); 274 | settings.setValue(QStringLiteral("hostName"), data.hostName); 275 | settings.setValue(QStringLiteral("userOverwrite"), data.userOverwrite); 276 | settings.setValue(QStringLiteral("remotePath"), data.remotePath); 277 | settings.setValue(QStringLiteral("localPath"), data.localPath); 278 | } 279 | settings.endArray(); 280 | } 281 | -------------------------------------------------------------------------------- /mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "editremotedialog.h" 2 | #include "mainwindow.h" 3 | #include "ui_mainwindow.h" 4 | #include 5 | #include 6 | #include 7 | 8 | MainWindow::MainWindow(QWidget *parent) : 9 | QMainWindow(parent), 10 | ui(new Ui::MainWindow), 11 | model(new MountModel(this)), 12 | sortModel(new QSortFilterProxyModel(this)), 13 | trayIco(new QSystemTrayIcon(windowIcon(), this)) 14 | { 15 | ui->setupUi(this); 16 | ui->treeView->setParent(this); 17 | centralWidget()->deleteLater(); 18 | setCentralWidget(ui->treeView); 19 | 20 | sortModel->setSourceModel(model); 21 | ui->treeView->setModel(sortModel); 22 | auto s = new QAction(this); 23 | s->setSeparator(true); 24 | ui->treeView->addActions({ 25 | ui->actionMount, 26 | ui->actionOpen_Folder, 27 | s, 28 | ui->actionEdit_Host, 29 | ui->actionRemove_Host 30 | }); 31 | 32 | trayIco->setToolTip(QApplication::applicationDisplayName()); 33 | auto menu = new QMenu(this); 34 | menu->addAction(QIcon::fromTheme(QStringLiteral("window-new")), tr("Show main window"), 35 | this, &MainWindow::show); 36 | menu->addMenu(model->createMountMenu(menu)); 37 | menu->addAction(ui->action_Reload_Mounts); 38 | menu->addSeparator(); 39 | auto runAction = menu->addAction(QIcon::fromTheme(QStringLiteral("games-config-options")), tr("Keep running"), 40 | qApp, [](bool triggered){ 41 | QApplication::setQuitOnLastWindowClosed(!triggered); 42 | }); 43 | runAction->setCheckable(true); 44 | auto startAction = menu->addAction(QIcon::fromTheme(QStringLiteral("system-run")), tr("Autostart"), 45 | this, &MainWindow::updateAutostart); 46 | startAction->setCheckable(true); 47 | menu->addAction(QIcon::fromTheme(QStringLiteral("gtk-quit")), tr("Quit"), 48 | qApp, &QApplication::quit); 49 | trayIco->setContextMenu(menu); 50 | trayIco->setVisible(true); 51 | 52 | QSettings settings; 53 | settings.beginGroup(QStringLiteral("gui")); 54 | restoreGeometry(settings.value(QStringLiteral("geom")).toByteArray()); 55 | restoreState(settings.value(QStringLiteral("state")).toByteArray()); 56 | ui->treeView->header()->restoreState(settings.value(QStringLiteral("header")).toByteArray()); 57 | runAction->setChecked(settings.value(QStringLiteral("background"), true).toBool()); 58 | startAction->setChecked(isAutostart()); 59 | QApplication::setQuitOnLastWindowClosed(!runAction->isChecked()); 60 | settings.endGroup(); 61 | 62 | connect(ui->actionExit, &QAction::triggered, 63 | qApp, &QApplication::quit); 64 | connect(ui->actionAbout_Qt, &QAction::triggered, 65 | qApp, &QApplication::aboutQt); 66 | connect(ui->action_Reload_Mounts, &QAction::triggered, 67 | model, &MountModel::reload); 68 | 69 | connect(model, &MountModel::modelReset, 70 | this, [this](){ 71 | reloadCurrent(QModelIndex()); 72 | }); 73 | connect(ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, 74 | this, &MainWindow::reloadCurrent); 75 | 76 | connect(model->controller(), &MountController::mountError, 77 | this, &MainWindow::mountError); 78 | 79 | connect(qApp, &QApplication::commitDataRequest, 80 | this, &MainWindow::commitShutdown); 81 | connect(qApp, &QApplication::saveStateRequest, 82 | this, &MainWindow::commitShutdown); 83 | } 84 | 85 | MainWindow::~MainWindow() 86 | { 87 | QSettings settings; 88 | settings.beginGroup(QStringLiteral("gui")); 89 | settings.setValue(QStringLiteral("geom"), saveGeometry()); 90 | settings.setValue(QStringLiteral("state"), saveState()); 91 | settings.setValue(QStringLiteral("header"), ui->treeView->header()->saveState()); 92 | settings.setValue(QStringLiteral("background"), !QApplication::quitOnLastWindowClosed()); 93 | settings.endGroup(); 94 | 95 | delete ui; 96 | } 97 | 98 | void MainWindow::mountError(const QString &name, const QString &errorLog, int exitCode) 99 | { 100 | reloadCurrent(ui->treeView->currentIndex()); 101 | 102 | auto conf = DialogMaster::createCritical(tr("Failed to mount/unmount %1").arg(name)); 103 | conf.parent = isVisible() ? this : nullptr; 104 | conf.title = conf.text; 105 | if(exitCode == -1){ 106 | conf.text = tr("The mount or unmount operation failed! Check the details for the " 107 | "generated error log"); 108 | } else { 109 | conf.text = tr("The mount or unmount operation failed with exit code %1! Check the details for the " 110 | "generated error log") 111 | .arg(exitCode); 112 | } 113 | conf.details = errorLog; 114 | DialogMaster::messageBox(conf); 115 | } 116 | 117 | void MainWindow::reloadCurrent(const QModelIndex &uiIndex) 118 | { 119 | auto index = sortModel->mapToSource(uiIndex); 120 | ui->actionMount->setEnabled(index.isValid()); 121 | 122 | if(index.isValid()) { 123 | auto mounted = model->isMounted(index); 124 | ui->actionEdit_Host->setEnabled(!mounted); 125 | ui->actionRemove_Host->setEnabled(!mounted); 126 | ui->actionMount->setChecked(mounted); 127 | } else { 128 | ui->actionEdit_Host->setEnabled(false); 129 | ui->actionRemove_Host->setEnabled(false); 130 | ui->actionMount->setChecked(false); 131 | } 132 | } 133 | 134 | void MainWindow::updateAutostart(bool checked) 135 | { 136 | auto resPath = QStringLiteral("%1/autostart/%2.sh") 137 | .arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)) 138 | .arg(QCoreApplication::applicationName()); 139 | if(checked) { 140 | QFile file(resPath); 141 | if(file.open(QIODevice::WriteOnly | QIODevice::Text)) { 142 | file.write(QStringLiteral("#!/bin/sh\n%1 --hidden") 143 | .arg(QCoreApplication::applicationName()) 144 | .toUtf8()); 145 | file.close(); 146 | file.setPermissions(file.permissions() | QFileDevice::ExeUser); 147 | } 148 | } else 149 | QFile::remove(resPath); 150 | } 151 | 152 | void MainWindow::commitShutdown(QSessionManager &sm) 153 | { 154 | auto args = sm.restartCommand(); 155 | if(!isVisible()) { 156 | if(!args.contains(QStringLiteral("--hidden"))) 157 | args.append(QStringLiteral("--hidden")); 158 | } else 159 | args.removeAll(QStringLiteral("--hidden")); 160 | sm.setRestartCommand(args); 161 | sm.setRestartHint(QSessionManager::RestartIfRunning); 162 | } 163 | 164 | void MainWindow::on_actionAdd_Host_triggered() 165 | { 166 | auto info = EditRemoteDialog::editInfo({}, this); 167 | if(info.isValid()) 168 | model->addMountInfo(info); 169 | } 170 | 171 | void MainWindow::on_actionEdit_Host_triggered() 172 | { 173 | auto index = sortModel->mapToSource(ui->treeView->currentIndex()); 174 | if(index.isValid()) { 175 | auto info = EditRemoteDialog::editInfo(model->mountInfo(index), this); 176 | if(info.isValid()) 177 | model->updateMountInfo(index, info); 178 | } 179 | } 180 | 181 | void MainWindow::on_actionRemove_Host_triggered() 182 | { 183 | auto index = sortModel->mapToSource(ui->treeView->currentIndex()); 184 | if(index.isValid()) { 185 | if(DialogMaster::question(this, tr("Do you really want to remove the selected mount?"))) 186 | model->removeMountInfo(index); 187 | } 188 | } 189 | 190 | void MainWindow::on_actionMount_triggered(bool checked) 191 | { 192 | auto index = sortModel->mapToSource(ui->treeView->currentIndex()); 193 | if(index.isValid()) { 194 | if(checked) 195 | model->mount(index); 196 | else 197 | model->unmount(index); 198 | } 199 | } 200 | 201 | void MainWindow::on_actionOpen_Folder_triggered() 202 | { 203 | auto index = sortModel->mapToSource(ui->treeView->currentIndex()); 204 | if(index.isValid()) { 205 | auto info = model->mountInfo(index); 206 | QDesktopServices::openUrl(QUrl::fromLocalFile(info.localPath)); 207 | } 208 | } 209 | 210 | void MainWindow::on_actionAbout_triggered() 211 | { 212 | DialogMaster::about(this, 213 | tr("A gui wrapper around sshfs"), 214 | true, 215 | QUrl(QStringLiteral("https://github.com/Skycoder42"))); 216 | } 217 | 218 | bool MainWindow::isAutostart() 219 | { 220 | auto resPath = QStringLiteral("%1/autostart/%2.sh") 221 | .arg(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)) 222 | .arg(QCoreApplication::applicationName()); 223 | return QFile::exists(resPath); 224 | } 225 | 226 | void MainWindow::on_treeView_activated(const QModelIndex &index) 227 | { 228 | auto srcIndex = sortModel->mapToSource(index); 229 | if(srcIndex.isValid()) { 230 | if(!model->isMounted(srcIndex)) 231 | model->mount(srcIndex); 232 | else { 233 | auto info = model->mountInfo(srcIndex); 234 | QDesktopServices::openUrl(QUrl::fromLocalFile(info.localPath)); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 575 10 | 394 11 | 12 | 13 | 14 | Qt::ToolButtonFollowStyle 15 | 16 | 17 | true 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | Qt::ActionsContextMenu 28 | 29 | 30 | QAbstractItemView::NoEditTriggers 31 | 32 | 33 | true 34 | 35 | 36 | true 37 | 38 | 39 | false 40 | 41 | 42 | false 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 0 55 | 0 56 | 575 57 | 23 58 | 59 | 60 | 61 | 62 | &File 63 | 64 | 65 | 66 | 67 | 68 | 69 | &Edit 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | &Action 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | &Help 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | TopToolBarArea 100 | 101 | 102 | false 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | .. 115 | 116 | 117 | &Exit 118 | 119 | 120 | 121 | 122 | 123 | .. 124 | 125 | 126 | &Add Host 127 | 128 | 129 | Ctrl+Ins 130 | 131 | 132 | 133 | 134 | false 135 | 136 | 137 | 138 | .. 139 | 140 | 141 | &Edit Host 142 | 143 | 144 | Ctrl+E 145 | 146 | 147 | 148 | 149 | false 150 | 151 | 152 | 153 | .. 154 | 155 | 156 | &Remove Host 157 | 158 | 159 | Del 160 | 161 | 162 | 163 | 164 | true 165 | 166 | 167 | false 168 | 169 | 170 | 171 | .. 172 | 173 | 174 | &Mount 175 | 176 | 177 | Ctrl+M 178 | 179 | 180 | 181 | 182 | false 183 | 184 | 185 | 186 | .. 187 | 188 | 189 | Open &Folder 190 | 191 | 192 | Ctrl+O 193 | 194 | 195 | 196 | 197 | 198 | .. 199 | 200 | 201 | Mount &all 202 | 203 | 204 | Ctrl+A, Ctrl+M 205 | 206 | 207 | 208 | 209 | 210 | .. 211 | 212 | 213 | &Unmount all 214 | 215 | 216 | Ctrl+A, Ctrl+U 217 | 218 | 219 | 220 | 221 | 222 | .. 223 | 224 | 225 | &About 226 | 227 | 228 | 229 | 230 | 231 | .. 232 | 233 | 234 | About &Qt 235 | 236 | 237 | 238 | 239 | 240 | .. 241 | 242 | 243 | &Reload Mounts 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | actionMount 252 | toggled(bool) 253 | actionOpen_Folder 254 | setEnabled(bool) 255 | 256 | 257 | -1 258 | -1 259 | 260 | 261 | -1 262 | -1 263 | 264 | 265 | 266 | 267 | actionMount 268 | toggled(bool) 269 | actionEdit_Host 270 | setDisabled(bool) 271 | 272 | 273 | -1 274 | -1 275 | 276 | 277 | -1 278 | -1 279 | 280 | 281 | 282 | 283 | actionMount 284 | toggled(bool) 285 | actionRemove_Host 286 | setDisabled(bool) 287 | 288 | 289 | -1 290 | -1 291 | 292 | 293 | -1 294 | -1 295 | 296 | 297 | 298 | 299 | 300 | --------------------------------------------------------------------------------