| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /** |
| * Copyright (c) 2013-2017, Regents of the University of California. |
| * |
| * This file is part of ChronoShare, a decentralized file sharing application over NDN. |
| * |
| * ChronoShare is free software: you can redistribute it and/or modify it under the terms |
| * of the GNU General Public License as published by the Free Software Foundation, either |
| * version 3 of the License, or (at your option) any later version. |
| * |
| * ChronoShare is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A |
| * PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| * |
| * You should have received copies of the GNU General Public License along with |
| * ChronoShare, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>. |
| * |
| * See AUTHORS.md for complete list of ChronoShare authors and contributors. |
| */ |
| |
| #include "chronosharegui.hpp" |
| #include "core/logging.hpp" |
| |
| #include <QDesktopServices> |
| #include <QDir> |
| #include <QFileInfo> |
| #include <QValidator> |
| |
| #include <boost/throw_exception.hpp> |
| #include <thread> |
| |
| namespace ndn { |
| namespace chronoshare { |
| |
| namespace fs = boost::filesystem; |
| |
| static const std::string HTTP_SERVER_ADDRESS = "localhost"; |
| static const std::string HTTP_SERVER_PORT = "9001"; |
| #ifdef _DEBUG |
| static const std::string DOC_ROOT = "gui/html"; |
| #else |
| static const std::string DOC_ROOT = ":/html"; |
| #endif |
| static const QString ICON_BIG_FILE(":/images/chronoshare-big.png"); |
| static const QString ICON_TRAY_FILE(":/images/" TRAY_ICON); |
| |
| _LOG_INIT(Gui); |
| |
| ChronoShareGui::ChronoShareGui(QWidget* parent) |
| : QDialog(parent) |
| , m_httpServer(0) |
| #ifdef SPARKLE_SUPPORTED |
| , m_autoUpdate( |
| new SparkleAutoUpdate(tr("http://irl.cs.ucla.edu/~zhenkai/chronoshare_dist/chronoshare.xml"))) |
| #endif |
| { |
| setWindowTitle("Settings"); |
| setMinimumWidth(600); |
| |
| labelUsername = new QLabel("Username(hint: /<username>)"); |
| labelSharedFolder = new QLabel("Shared Folder Name"); |
| labelSharedFolderPath = new QLabel("Shared Folder Path"); |
| |
| QRegExp regex("(/[^/]+)+$"); |
| QValidator* prefixValidator = new QRegExpValidator(regex, this); |
| |
| editUsername = new QLineEdit(); |
| editUsername->setValidator(prefixValidator); |
| |
| QRegExp noPureWhiteSpace("^\\S+.*$"); |
| QValidator* wsValidator = new QRegExpValidator(noPureWhiteSpace, this); |
| editSharedFolder = new QLineEdit(); |
| editSharedFolder->setValidator(wsValidator); |
| |
| editSharedFolderPath = new QLineEdit(); |
| editSharedFolderPath->setReadOnly(true); |
| QPalette pal = editSharedFolderPath->palette(); |
| pal.setColor(QPalette::Active, QPalette::Base, pal.color(QPalette::Disabled, QPalette::Base)); |
| editSharedFolderPath->setPalette(pal); |
| button = new QPushButton("Save and apply settings"); |
| |
| QString versionString = QString("Version: ChronoShare v%1").arg(CHRONOSHARE_VERSION); |
| label = new QLabel(versionString, this); |
| |
| connect(button, SIGNAL(clicked()), this, SLOT(changeSettings())); |
| |
| mainLayout = new QVBoxLayout; // vertically |
| mainLayout->addWidget(labelUsername); |
| mainLayout->addWidget(editUsername); |
| mainLayout->addWidget(labelSharedFolder); |
| mainLayout->addWidget(editSharedFolder); |
| mainLayout->addWidget(labelSharedFolderPath); |
| mainLayout->addWidget(editSharedFolderPath); |
| mainLayout->addWidget(button); |
| mainLayout->addWidget(label); |
| setLayout(mainLayout); |
| |
| // create actions that result from clicking a menu option |
| createActionsAndMenu(); |
| |
| // create tray icon |
| createTrayIcon(); |
| |
| // set icon image |
| setIcon(); |
| |
| // show tray icon |
| m_trayIcon->show(); |
| |
| // load settings |
| if (!loadSettings() || m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() || |
| m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") { |
| // prompt user to choose folder |
| _LOG_DEBUG("First Time Setup"); |
| openMessageBox("First Time Setup", |
| "Please enter a username, shared folder name and choose the shared folder path on your local filesystem."); |
| viewSettings(); |
| openFileDialog(); |
| viewSettings(); |
| } |
| else { |
| startBackend(); |
| } |
| } |
| |
| ChronoShareGui::ChronoShareGui(QString dirPath, QString username, QString sharedFolderName, QWidget* parent) |
| : QDialog(parent) |
| , m_dirPath(dirPath) |
| , m_username(username) |
| , m_sharedFolderName(sharedFolderName) |
| { |
| if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() || |
| m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") { |
| // prompt user to choose folder |
| BOOST_THROW_EXCEPTION(Error("Some error with init info")); |
| } |
| else { |
| startBackend(); |
| } |
| } |
| |
| void |
| ChronoShareGui::startBackend(bool restart /*=false*/) |
| { |
| if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() || |
| m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") { |
| _LOG_DEBUG("Don't start backend, because settings are in progress or incomplete"); |
| return; |
| } |
| |
| if (m_watcher != 0 && m_dispatcher != 0) { |
| if (!restart) { |
| return; |
| } |
| |
| _LOG_DEBUG("Restarting Dispatcher and FileWatcher for the new location or new username"); |
| m_watcher.reset(); // stop filewatching ASAP |
| m_dispatcher.reset(); // stop dispatcher ASAP, but after watcher(to prevent triggering callbacks |
| // on deleted object) |
| m_ioServiceWork.reset(); |
| m_ioServiceManager->handle_stop(); |
| m_NetworkThread.join(); |
| delete m_ioServiceManager; |
| } |
| |
| fs::path realPathToFolder(m_dirPath.toStdString()); |
| realPathToFolder /= m_sharedFolderName.toStdString(); |
| |
| std::cout << "m_dispatcher username:" << m_username.toStdString() |
| << " m_sharedFolderName:" << m_sharedFolderName.toStdString() |
| << " realPathToFolder: " << realPathToFolder << std::endl; |
| |
| m_ioService.reset(new boost::asio::io_service()); |
| m_face.reset(new Face(*m_ioService)); |
| m_dispatcher.reset(new Dispatcher(m_username.toStdString(), m_sharedFolderName.toStdString(), |
| realPathToFolder, *m_face)); |
| |
| // Alex: this **must** be here, otherwise m_dirPath will be uninitialized |
| m_watcher.reset(new FsWatcher(*m_ioService, realPathToFolder.string().c_str(), |
| bind(&Dispatcher::Did_LocalFile_AddOrModify, m_dispatcher.get(), _1), |
| bind(&Dispatcher::Did_LocalFile_Delete, m_dispatcher.get(), _1))); |
| try { |
| m_ioServiceManager = new IoServiceManager(*m_ioService); |
| m_NetworkThread = std::thread(&IoServiceManager::run, m_ioServiceManager); |
| } |
| catch (const std::exception& e) { |
| _LOG_ERROR("Start IO service or Face failed"); |
| openWarningMessageBox("", "WARNING: Cannot allocate thread for face and io_service!", |
| QString("Starting chronoshare failed" |
| "Exception caused: %1") |
| .arg(e.what())); |
| // stop filewatching ASAP |
| m_watcher.reset(); |
| m_dispatcher.reset(); |
| return; |
| } |
| |
| if (m_httpServer != 0) { |
| // no need to restart webserver if it already exists |
| return; |
| } |
| |
| QFileInfo indexHtmlInfo(":/html/index.html"); |
| if (indexHtmlInfo.exists()) { |
| try { |
| m_httpServer = new http::server::server(HTTP_SERVER_ADDRESS, HTTP_SERVER_PORT, DOC_ROOT); |
| m_httpServerThread = std::thread(&http::server::server::run, m_httpServer); |
| } |
| catch (const std::exception& e) { |
| _LOG_ERROR("Start http server failed"); |
| m_httpServer = 0; // just to make sure |
| openWarningMessageBox("WARNING", "WARNING: Cannot start http server!", |
| QString("Starting http server failed. You will " |
| "not be able to check history from web " |
| "brower. Exception caused: %1") |
| .arg(e.what())); |
| } |
| } |
| else { |
| _LOG_ERROR("Http server doc root dir does not exist!"); |
| } |
| } |
| |
| ChronoShareGui::~ChronoShareGui() |
| { |
| // stop filewatching ASAP |
| m_watcher.reset(); |
| |
| // stop dispatcher ASAP, but after watcher to prevent triggering callbacks on the deleted object |
| m_dispatcher.reset(); |
| |
| m_ioServiceWork.reset(); |
| m_ioServiceManager->handle_stop(); |
| m_NetworkThread.join(); |
| delete m_ioServiceManager; |
| |
| if (m_httpServer != 0) { |
| m_httpServer->handle_stop(); |
| m_httpServerThread.join(); |
| delete m_httpServer; |
| } |
| |
| // cleanup |
| delete m_trayIcon; |
| delete m_trayIconMenu; |
| #ifdef SPARKLE_SUPPORTED |
| delete m_autoUpdate; |
| delete m_checkForUpdates; |
| #endif |
| delete m_openFolder; |
| delete m_viewSettings; |
| delete m_changeFolder; |
| delete m_quitProgram; |
| |
| delete labelUsername; |
| delete labelSharedFolder; |
| delete editUsername; |
| delete editSharedFolder; |
| delete button; |
| delete label; |
| delete mainLayout; |
| |
| // to avoid `private field 'm_checkForUpdates' is not used` warning/error |
| (void)(m_checkForUpdates); |
| } |
| |
| void |
| ChronoShareGui::openMessageBox(QString title, QString text) |
| { |
| QMessageBox messageBox(this); |
| messageBox.setWindowTitle(title); |
| messageBox.setText(text); |
| |
| messageBox.setIconPixmap(QPixmap(ICON_BIG_FILE)); |
| |
| messageBox.exec(); |
| } |
| |
| void |
| ChronoShareGui::openMessageBox(QString title, QString text, QString infotext) |
| { |
| QMessageBox messageBox(this); |
| messageBox.setWindowTitle(title); |
| messageBox.setText(text); |
| messageBox.setInformativeText(infotext); |
| |
| messageBox.setIconPixmap(QPixmap(ICON_BIG_FILE)); |
| |
| messageBox.exec(); |
| } |
| |
| void |
| ChronoShareGui::openWarningMessageBox(QString title, QString text, QString infotext) |
| { |
| QMessageBox messageBox(this); |
| messageBox.setWindowTitle(title); |
| messageBox.setText(text); |
| messageBox.setInformativeText(infotext); |
| |
| messageBox.setIcon(QMessageBox::Warning); |
| messageBox.setStandardButtons(QMessageBox::Ok); |
| |
| messageBox.exec(); |
| } |
| |
| void |
| ChronoShareGui::createActionsAndMenu() |
| { |
| _LOG_DEBUG("Create actions"); |
| |
| // create the "open folder" action |
| m_openFolder = new QAction(tr("&Open Folder"), this); |
| connect(m_openFolder, SIGNAL(triggered()), this, SLOT(openSharedFolder())); |
| |
| m_openWeb = new QAction(tr("Open in &Browser"), this); |
| connect(m_openWeb, SIGNAL(triggered()), this, SLOT(openInWebBrowser())); |
| |
| m_recentFilesMenu = new QMenu(tr("Recently Changed Files"), this); |
| for (int i = 0; i < 5; i++) { |
| m_fileActions[i] = new QAction(this); |
| m_fileActions[i]->setVisible(false); |
| connect(m_fileActions[i], SIGNAL(triggered()), this, SLOT(openFile())); |
| m_recentFilesMenu->addAction(m_fileActions[i]); |
| } |
| connect(m_recentFilesMenu, SIGNAL(aboutToShow()), this, SLOT(updateRecentFilesMenu())); |
| |
| // create the "view settings" action |
| m_viewSettings = new QAction(tr("&View Settings"), this); |
| connect(m_viewSettings, SIGNAL(triggered()), this, SLOT(viewSettings())); |
| |
| // create the "change folder" action |
| m_changeFolder = new QAction(tr("&Change Folder"), this); |
| connect(m_changeFolder, SIGNAL(triggered()), this, SLOT(openFileDialog())); |
| |
| #ifdef SPARKLE_SUPPORTED |
| m_checkForUpdates = new QAction(tr("Check For Updates"), this); |
| connect(m_checkForUpdates, SIGNAL(triggered()), this, SLOT(onCheckForUpdates())); |
| #endif |
| |
| // create the "quit program" action |
| m_quitProgram = new QAction(tr("&Quit"), this); |
| connect(m_quitProgram, SIGNAL(triggered()), qApp, SLOT(quit())); |
| } |
| |
| void |
| ChronoShareGui::createTrayIcon() |
| { |
| // create a new icon menu |
| m_trayIconMenu = new QMenu(this); |
| |
| // add actions to the menu |
| m_trayIconMenu->addAction(m_openFolder); |
| m_trayIconMenu->addAction(m_openWeb); |
| m_trayIconMenu->addMenu(m_recentFilesMenu); |
| m_trayIconMenu->addSeparator(); |
| m_trayIconMenu->addAction(m_viewSettings); |
| m_trayIconMenu->addAction(m_changeFolder); |
| |
| #ifdef SPARKLE_SUPPORTED |
| m_trayIconMenu->addSeparator(); |
| m_trayIconMenu->addAction(m_checkForUpdates); |
| #endif |
| |
| m_trayIconMenu->addSeparator(); |
| m_trayIconMenu->addAction(m_quitProgram); |
| |
| // create new tray icon |
| m_trayIcon = new QSystemTrayIcon(this); |
| |
| // associate the menu with the tray icon |
| m_trayIcon->setContextMenu(m_trayIconMenu); |
| |
| // handle left click of icon |
| connect(m_trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, |
| SLOT(trayIconClicked(QSystemTrayIcon::ActivationReason))); |
| } |
| |
| void |
| ChronoShareGui::onCheckForUpdates() |
| { |
| #ifdef SPARKLE_SUPPORTED |
| cout << "+++++++++++ trying to update +++++++ " << endl; |
| m_autoUpdate->checkForUpdates(); |
| cout << "+++++++++++ end trying to update +++++++ " << endl; |
| #endif |
| } |
| |
| void |
| ChronoShareGui::setIcon() |
| { |
| // set the icon image |
| m_trayIcon->setIcon(QIcon(ICON_TRAY_FILE)); |
| } |
| |
| void |
| ChronoShareGui::openSharedFolder() |
| { |
| fs::path realPathToFolder(m_dirPath.toStdString()); |
| realPathToFolder /= m_sharedFolderName.toStdString(); |
| QString path = QDir::toNativeSeparators(realPathToFolder.string().c_str()); |
| QDesktopServices::openUrl(QUrl("file:///" + path)); |
| } |
| |
| void |
| ChronoShareGui::openInWebBrowser() |
| { |
| QUrl url("http://localhost:9001/"); |
| url.setFragment("folderHistory&" |
| "user=" + |
| QUrl::toPercentEncoding(m_username) + "&" |
| "folder=" + |
| QUrl::toPercentEncoding(m_sharedFolderName)); |
| |
| // i give up. there is double encoding and I have no idea how to fight it... |
| QDesktopServices::openUrl(url); |
| } |
| |
| void |
| ChronoShareGui::openFile() |
| { |
| // figure out who sent the signal |
| QAction* pAction = qobject_cast<QAction*>(sender()); |
| if (pAction->isEnabled()) { |
| // we stored full path of the file in this toolTip field |
| #ifdef Q_WS_MAC |
| // we do some hack so we could show the file in Finder highlighted |
| QStringList args; |
| args << "-e"; |
| args << "tell application \"Finder\""; |
| args << "-e"; |
| args << "activate"; |
| args << "-e"; |
| args << "select POSIX file \"" + pAction->toolTip() + "/" + pAction->text() + "\""; |
| args << "-e"; |
| args << "end tell"; |
| QProcess::startDetached("osascript", args); |
| #else |
| // too bad qt couldn't do highlighting for Linux(or Mac) |
| QDesktopServices::openUrl(QUrl("file:///" + pAction->toolTip())); |
| #endif |
| } |
| } |
| |
| void |
| ChronoShareGui::updateRecentFilesMenu() |
| { |
| for (int i = 0; i < 5; i++) { |
| m_fileActions[i]->setVisible(false); |
| } |
| m_dispatcher->LookupRecentFileActions(std::bind(&ChronoShareGui::checkFileAction, this, _1, _2, _3), |
| 5); |
| } |
| |
| void |
| ChronoShareGui::checkFileAction(const std::string& filename, int action, int index) |
| { |
| fs::path realPathToFolder(m_dirPath.toStdString()); |
| realPathToFolder /= m_sharedFolderName.toStdString(); |
| realPathToFolder /= filename; |
| QString fullPath = realPathToFolder.string().c_str(); |
| QFileInfo fileInfo(fullPath); |
| if (fileInfo.exists()) { |
| // This is a hack, we just use some field to store the path |
| m_fileActions[index]->setToolTip(fileInfo.absolutePath()); |
| m_fileActions[index]->setEnabled(true); |
| } |
| else { |
| // after half an hour frustrating test and search around, |
| // I think it's the problem of Qt. |
| // According to the Qt doc, the action cannot be clicked |
| // and the area would be grey, but it didn't happen |
| // User can still trigger the action, and not greyed |
| // added check in SLOT to see if the action is "enalbed" |
| // as a remedy |
| // Give up at least for now |
| m_fileActions[index]->setEnabled(false); |
| // UPDATE, file not fetched yet |
| if (action == 0) { |
| QFont font; |
| // supposed by change the font, didn't happen |
| font.setWeight(QFont::Light); |
| m_fileActions[index]->setFont(font); |
| m_fileActions[index]->setToolTip(tr("Fetching...")); |
| } |
| // DELETE |
| else { |
| QFont font; |
| // supposed by change the font, didn't happen |
| font.setStrikeOut(true); |
| m_fileActions[index]->setFont(font); |
| m_fileActions[index]->setToolTip(tr("Deleted...")); |
| } |
| } |
| m_fileActions[index]->setText(fileInfo.fileName()); |
| m_fileActions[index]->setVisible(true); |
| } |
| |
| void |
| ChronoShareGui::changeSettings() |
| { |
| QString oldUsername = m_username; |
| QString oldSharedFolderName = m_sharedFolderName; |
| |
| if (!editUsername->text().isEmpty()) |
| m_username = editUsername->text().trimmed(); |
| else |
| editUsername->setText(m_username); |
| |
| if (!editSharedFolder->text().isEmpty()) |
| m_sharedFolderName = editSharedFolder->text().trimmed(); |
| else |
| editSharedFolder->setText(m_sharedFolderName); |
| |
| if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() || |
| m_sharedFolderName == "") { |
| openMessageBox("Error", "Username and shared folder name cannot be empty"); |
| } |
| else { |
| saveSettings(); |
| this->hide(); |
| |
| if (m_username != oldUsername || oldSharedFolderName != m_sharedFolderName) { |
| startBackend(true); // restart dispatcher/fswatcher |
| } |
| } |
| } |
| |
| void |
| ChronoShareGui::openFileDialog() |
| { |
| while (true) { |
| // prompt user for new directory |
| QString tempPath = |
| QFileDialog::getExistingDirectory(this, tr("Choose ChronoShare folder"), m_dirPath, |
| QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); |
| if (tempPath.isNull()) { |
| if (m_dirPath.isNull()) { |
| openMessageBox("Notice", |
| "ChronoShare will use [" + QDir::homePath() + |
| "/ChronoShare]. \n\nYou can change it later selecting \"Change Folder\" menu."); |
| |
| m_dirPath = QDir::homePath() + "/ChronoShare"; |
| editSharedFolderPath->setText(m_dirPath); |
| break; |
| } |
| else { |
| // user just cancelled, no need to do anything else |
| return; |
| } |
| } |
| |
| QFileInfo qFileInfo(tempPath); |
| |
| if (!qFileInfo.isDir()) { |
| openMessageBox("Error", "Please select an existing folder or create a new one."); |
| continue; |
| } |
| |
| if (m_dirPath == tempPath) { |
| // user selected the same directory, no need to do anything |
| return; |
| } |
| |
| m_dirPath = tempPath; |
| editSharedFolderPath->setText(m_dirPath); |
| break; |
| } |
| |
| _LOG_DEBUG("Selected path: " << m_dirPath.toStdString()); |
| // save settings |
| saveSettings(); |
| |
| startBackend(true); // restart dispatcher/fswatcher |
| } |
| |
| void |
| ChronoShareGui::trayIconClicked(QSystemTrayIcon::ActivationReason reason) |
| { |
| // if double clicked, open shared folder |
| if (reason == QSystemTrayIcon::DoubleClick) { |
| openSharedFolder(); |
| } |
| } |
| |
| void |
| ChronoShareGui::viewSettings() |
| { |
| //simple for now |
| this->show(); |
| this->raise(); |
| this->activateWindow(); |
| } |
| |
| bool |
| ChronoShareGui::loadSettings() |
| { |
| bool successful = true; |
| |
| // Load Settings |
| // QSettings settings(m_settingsFilePath, QSettings::NativeFormat); |
| QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "irl.cs.ucla.edu", "ChronoShare"); |
| |
| if (settings.contains("username")) { |
| m_username = settings.value("username", "admin").toString(); |
| } |
| else { |
| successful = false; |
| } |
| |
| editUsername->setText(m_username); |
| |
| if (settings.contains("sharedfoldername")) { |
| m_sharedFolderName = settings.value("sharedfoldername", "shared").toString(); |
| } |
| else { |
| successful = false; |
| } |
| |
| editSharedFolder->setText(m_sharedFolderName); |
| |
| if (settings.contains("dirPath")) { |
| m_dirPath = settings.value("dirPath", QDir::homePath()).toString(); |
| } |
| else { |
| successful = false; |
| } |
| |
| editSharedFolderPath->setText(m_dirPath); |
| |
| _LOG_DEBUG("Found configured path: " << (successful ? m_dirPath.toStdString() : std::string("no"))); |
| |
| return successful; |
| } |
| |
| void |
| ChronoShareGui::saveSettings() |
| { |
| // Save Settings |
| // QSettings settings(m_settingsFilePath, QSettings::NativeFormat); |
| QSettings settings(QSettings::NativeFormat, QSettings::UserScope, "irl.cs.ucla.edu", "ChronoShare"); |
| |
| settings.setValue("dirPath", m_dirPath); |
| settings.setValue("username", m_username); |
| settings.setValue("sharedfoldername", m_sharedFolderName); |
| } |
| |
| void |
| ChronoShareGui::closeEvent(QCloseEvent* event) |
| { |
| _LOG_DEBUG("Close Event"); |
| |
| if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() || |
| m_sharedFolderName == "") { |
| openMessageBox("ChronoShare is not active", |
| "Username and/or shared folder name are not set.\n\n To activate ChronoShare, " |
| "open Settings menu and configure your username and shared folder name"); |
| } |
| |
| this->hide(); |
| event->ignore(); // don't let the event propagate to the base class |
| } |
| |
| } // namespace chronoshare |
| } // namespace ndn |
| |
| #include "chronosharegui.moc" |